I really need some help. I'm trying to make my own Timeline chart (the same as google timeline). It works already but I can not figure out how to make a function for grouping labels by type. I hardcoded this function and it works for 3 groups/types. What I need is to make a universal function that groups all labels even if there are 100 of them. The second problem is that in the second and third groups are not in the same line if a.endTime >= b.startTime. I will be very thankful for every help
Here is my code:
var w = 800;
var h = 400;
var svg = d3
.select(".svg")
.append("svg")
.attr("width", w)
.attr("height", h)
.attr("class", "svg");
//main array
var items = [{
task: "conceptualize",
type: "development",
startTime: "2013-1-28", //year/month/day
endTime: "2013-2-1",
number: 2,
},
{
task: "sketch",
type: "development",
startTime: "2013-2-6",
endTime: "2013-2-9",
number: 2,
},
{
task: "color profiles",
type: "development",
startTime: "2013-2-6",
endTime: "2013-2-9",
number: 2,
},
{
task: "HTML",
type: "coding",
startTime: "2013-2-2",
endTime: "2013-2-6",
number: 1,
},
{
task: "write the JS",
type: "coding",
startTime: "2013-2-1",
endTime: "2013-2-6",
number: 1,
},
{
task: "eat",
type: "celebration",
startTime: "2013-2-8",
endTime: "2013-2-13",
number: 0,
},
{
task: "crying",
type: "celebration",
startTime: "2013-2-13",
endTime: "2013-2-16",
number: 0,
},
];
//array sorting
items.sort((a, b) => {
return (
a.number - b.number || Date.parse(b.startTime) - Date.parse(a.startTime)
);
});
//here create new array and add index for every label
var taskArray = [];
var stack = [];
items.map((e) => {
var lane = stack.findIndex(
(s) => s.endTime <= e.startTime && s.startTime < e.startTime
);
var yIndex = lane === -1 ? stack.length : lane;
taskArray.push({
...e,
yIndex,
});
stack[yIndex] = e;
});
//here hardcoded grouping based on index and type
var nRows = d3.max(taskArray, (d) =>
d.type === "celebration" ? d.yIndex + 1 : null
);
//here hardcoded grouping based on index and type
var nRows2 = d3.max(taskArray, (d) =>
d.type === "development" ? (d.yIndex = d.yIndex + 1 + nRows) : null
);
var dateFormat = d3.time.format("%Y-%m-%d");
var timeScale = d3.time
.scale()
.domain([
d3.min(taskArray, function(d) {
return dateFormat.parse(d.startTime);
}),
d3.max(taskArray, function(d) {
return dateFormat.parse(d.endTime);
}),
])
.range([0, w - 150]);
var categories = new Array();
for (var i = 0; i < taskArray.length; i++) {
categories.push(taskArray[i].type);
}
var catsUnfiltered = categories; //for vert labels
categories = checkUnique(categories);
makeGant(taskArray, w, h);
function makeGant(tasks, pageWidth, pageHeight) {
var barHeight = 20;
var gap = barHeight + 4;
var topPadding = 75;
var sidePadding = 75;
var colorScale = d3.scale
.linear()
.domain([0, categories.length])
.range(["#00B9FA", "#F95002"])
.interpolate(d3.interpolateHcl);
makeGrid(sidePadding, topPadding, pageWidth, pageHeight);
drawRects(
tasks,
gap,
topPadding,
sidePadding,
barHeight,
colorScale,
pageWidth,
pageHeight
);
vertLabels(gap, topPadding, sidePadding, barHeight, colorScale);
}
function drawRects(
theArray,
theGap,
theTopPad,
theSidePad,
theBarHeight,
theColorScale,
w,
h
) {
svg
.append("g")
.selectAll("rect")
.data(theArray)
.enter()
.append("rect")
.attr("x", 0)
.attr("y", function(d, i) {
return d.yIndex * theGap + theTopPad - 2;
})
.attr("width", function(d) {
return w - theSidePad / 2;
})
.attr("height", theGap)
.attr("stroke", "none")
.attr("fill", function(d) {
for (var i = 0; i < categories.length; i++) {
if (d.type == categories[i]) {
return d3.rgb(theColorScale(i));
}
}
})
.attr("opacity", 0.2);
var rectangles = svg.append("g").selectAll("rect").data(theArray).enter();
rectangles
.append("rect")
.attr("rx", 3)
.attr("ry", 3)
.attr("x", function(d) {
return timeScale(dateFormat.parse(d.startTime)) + theSidePad;
})
//here draw milestones depend on index
.attr("y", function(d, i) {
return d.yIndex * theGap + theTopPad;
})
.attr("width", function(d) {
return (
timeScale(dateFormat.parse(d.endTime)) -
timeScale(dateFormat.parse(d.startTime))
);
})
.attr("height", theBarHeight)
.attr("stroke", "none")
.attr("fill", function(d) {
for (var i = 0; i < categories.length; i++) {
if (d.type == categories[i]) {
return d3.rgb(theColorScale(i));
}
}
});
rectangles
.append("text")
.text(function(d) {
return d.task;
})
.attr("x", function(d) {
return (
(timeScale(dateFormat.parse(d.endTime)) -
timeScale(dateFormat.parse(d.startTime))) /
2 +
timeScale(dateFormat.parse(d.startTime)) +
theSidePad
);
})
.attr("y", function(d, i) {
return d.yIndex * theGap + 14 + theTopPad;
})
.attr("font-size", 11)
.attr("text-anchor", "middle")
.attr("text-height", theBarHeight)
.attr("fill", "#fff");
}
function makeGrid(theSidePad, theTopPad, w, h) {
var xAxis = d3.svg
.axis()
.scale(timeScale)
.orient("bottom")
.ticks(d3.time.days, 1)
.tickSize(-h + theTopPad + 20, 0, 0)
.tickFormat(d3.time.format("%d %b"));
var grid = svg
.append("g")
.attr("class", "grid")
.attr("transform", "translate(" + theSidePad + ", " + (h - 50) + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "middle")
.attr("fill", "#000")
.attr("stroke", "none")
.attr("font-size", 10)
.attr("dy", "1em");
}
function vertLabels(
theGap,
theTopPad,
theSidePad,
theBarHeight,
theColorScale
) {
var numOccurances = new Array();
var prevGap = 0;
for (var i = 0; i < categories.length; i++) {
numOccurances[i] = [categories[i], getCount(categories[i], catsUnfiltered)];
}
var axisText = svg
.append("g") //without doing this, impossible to put grid lines behind text
.selectAll("text")
.data(numOccurances)
.enter()
.append("text")
.text(function(d) {
return d[0];
})
.attr("x", 10)
.attr("y", function(d, i) {
if (i > 0) {
for (var j = 0; j < i; j++) {
prevGap += numOccurances[i - 1][1];
// console.log(prevGap);
return (d[1] * theGap) / 2 + prevGap * theGap + theTopPad;
}
} else {
return (d[1] * theGap) / 2 + theTopPad;
}
})
.attr("font-size", 11)
.attr("text-anchor", "start")
.attr("text-height", 14)
.attr("fill", function(d) {
for (var i = 0; i < categories.length; i++) {
if (d[0] == categories[i]) {
// console.log("true!");
return d3.rgb(theColorScale(i)).darker();
}
}
});
}
//from this stackexchange question: http://stackoverflow.com/questions/1890203/unique-for-arrays-in-javascript
function checkUnique(arr) {
var hash = {},
result = [];
for (var i = 0, l = arr.length; i < l; ++i) {
if (!hash.hasOwnProperty(arr[i])) {
//it works with objects! in FF, at least
hash[arr[i]] = true;
result.push(arr[i]);
}
}
return result;
}
//from this stackexchange question: http://stackoverflow.com/questions/14227981/count-how-many-strings-in-an-array-have-duplicates-in-the-same-array
function getCounts(arr) {
var i = arr.length, // var to loop over
obj = {}; // obj to store results
while (i) obj[arr[--i]] = (obj[arr[i]] || 0) + 1; // count occurrences
return obj;
}
// get specific from everything
function getCount(word, arr) {
return getCounts(arr)[word] || 0;
}
body {
background: #fff;
font-family: 'Open-Sans', sans-serif;
}
#container {
margin: 0 auto;
position: relative;
width: 800px;
overflow: visible;
}
.svg {
width: 800px;
height: 400px;
overflow: visible;
position: absolute;
}
.grid .tick {
stroke: lightgrey;
opacity: 0.3;
shape-rendering: crispEdges;
}
.grid path {
stroke-width: 0;
}
<div id="container">
<div class="svg"></div>
</div>
<script src="https://d3js.org/d3.v3.min.js"></script>
You can massively simplify your code, while making it dynamic. Some pointers in addition to the comments I left
Just parse your date objects ASAP, instead of doing it every time you need them;
Use d3.nest to group the categories;
Don't iterate over the categories to get the index for colouring, just use the second function argument (d, i);
Draw one g element per category, one rect.background within the g, and the tasks that fit that category as well. That way, you can remove categories extremely easily, it makes it much easier to calculate position, and you need to set the fill colours only once.
Let me know if you have any questions
var w = 800;
var h = 400;
var svg = d3
.select(".svg")
.append("svg")
.attr("width", w)
.attr("height", h)
.attr("class", "svg");
//main array
var items = [{
task: "conceptualize",
type: "development",
startTime: "2013-1-28", //year/month/day
endTime: "2013-2-1",
number: 2,
},
{
task: "sketch",
type: "development",
startTime: "2013-2-6",
endTime: "2013-2-9",
number: 2,
},
{
task: "color profiles",
type: "development",
startTime: "2013-2-6",
endTime: "2013-2-9",
number: 2,
},
{
task: "HTML",
type: "coding",
startTime: "2013-2-2",
endTime: "2013-2-6",
number: 1,
},
{
task: "write the JS",
type: "coding",
startTime: "2013-2-1",
endTime: "2013-2-6",
number: 1,
},
{
task: "eat",
type: "celebration",
startTime: "2013-2-8",
endTime: "2013-2-13",
number: 0,
},
{
task: "crying",
type: "celebration",
startTime: "2013-2-13",
endTime: "2013-2-16",
number: 0,
},
];
var dateFormat = d3.time.format("%Y-%m-%d");
items.forEach((e) => {
e.startTime = dateFormat.parse(e.startTime);
e.endTime = dateFormat.parse(e.endTime);
});
// Use d3.nest to group the items based on the category
// https://github.com/d3/d3-3.x-api-reference/blob/master/Arrays.md#d3_nest
// You can use .sortKeys to sort by key, for example
var categories = d3.nest()
.key(d => d.type)
.sortValues((a, b) => a.startTime - b.startTime)
.rollup(function(events) {
// Here, we see if we can apply some useful logic
// In this case, we search for overlapping events so they can be placed
// in different lanes
events.forEach(function(e, i) {
// Look only at the preceding events
// Remember that the events have been sorted already
const overlappingEvents = events.slice(0, i)
.filter(other => other.endTime > e.startTime);
if(overlappingEvents.length > 0) {
e.level = d3.max(overlappingEvents, e => e.level) + 1;
} else {
e.level = 0;
}
});
return events;
})
.entries(items);
// Set for each category the required number of lanes
let offset = 0;
categories.forEach(c => {
c.lanes = d3.max(c.values, d => d.level) + 1;
c.offset = offset;
offset += c.lanes;
});
var timeScale = d3.time
.scale()
.domain([
d3.min(items, d => d.startTime),
d3.max(items, d => d.endTime)
])
.range([0, w - 150]);
makeGant(categories, w, h);
function makeGant(categories, pageWidth, pageHeight) {
var barHeight = 20;
var gap = barHeight + 4;
var topPadding = 75;
var sidePadding = 75;
var colorScale = d3.scale
.linear()
.domain([0, categories.length])
.range(["#00B9FA", "#F95002"])
.interpolate(d3.interpolateHcl);
makeGrid(sidePadding, topPadding, pageWidth, pageHeight);
drawRects(
categories,
gap,
topPadding,
sidePadding,
barHeight,
colorScale,
pageWidth,
pageHeight
);
vertLabels(gap, topPadding, sidePadding, barHeight, colorScale);
}
function drawRects(
categories,
theGap,
theTopPad,
theSidePad,
theBarHeight,
theColorScale,
w,
h
) {
const categoryGroups = svg
.append("g")
.selectAll("g")
.data(categories)
.enter()
.append("g")
.attr("transform", function(d, i) {
// Take the preceding categories and sum their required number of levels
return `translate(0, ${d.offset * theGap + theTopPad - 2})`;
})
// All children inherit this attribute
.attr("fill", function(d, i) {
return d3.rgb(theColorScale(i));
})
// Just draw one rectangle for all lanes of this category
categoryGroups
.append("rect")
.attr("class", "background")
.attr("width", function(d) {
return w - theSidePad / 2;
})
.attr("height", function(d) {
return theGap * d.lanes;
})
.attr("stroke", "none")
.attr("opacity", 0.2);
//
var rectangles = categoryGroups
.selectAll(".task")
.data(function(d) { return d.values; })
.enter();
rectangles
.append("rect")
.attr("class", "task")
.attr("rx", 3)
.attr("ry", 3)
.attr("x", function(d) {
return timeScale(d.startTime) + theSidePad;
})
//here draw milestones depend on index
.attr("y", function(d, i) {
return d.level * theGap;
})
.attr("width", function(d) {
return timeScale(d.endTime) - timeScale(d.startTime);
})
.attr("height", theBarHeight)
.attr("stroke", "none");
rectangles
.append("text")
.text(function(d) {
return d.task;
})
.attr("x", function(d) {
return (timeScale(d.endTime) - timeScale(d.startTime)) / 2 +
timeScale(d.startTime) + theSidePad;
})
.attr("y", function(d, i) {
return d.level * theGap + 14;
})
.attr("font-size", 11)
.attr("text-anchor", "middle")
.attr("text-height", theBarHeight)
.attr("fill", "#fff");
}
function makeGrid(theSidePad, theTopPad, w, h) {
var xAxis = d3.svg
.axis()
.scale(timeScale)
.orient("bottom")
.ticks(d3.time.days, 1)
.tickSize(-h + theTopPad + 20, 0, 0)
.tickFormat(d3.time.format("%d %b"));
var grid = svg
.append("g")
.attr("class", "grid")
.attr("transform", "translate(" + theSidePad + ", " + (h - 50) + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "middle")
.attr("fill", "#000")
.attr("stroke", "none")
.attr("font-size", 10)
.attr("dy", "1em");
}
function vertLabels(
theGap,
theTopPad,
theSidePad,
theBarHeight,
theColorScale
) {
var axisText = svg
.append("g") //without doing this, impossible to put grid lines behind text
.selectAll("text")
.data(categories) // one label per category
.enter()
.append("text")
.text(function(d) {
return d.key;
})
.attr("x", 10)
.attr("y", function(d, i) {
return (d.offset + d.lanes / 2) * theGap + theTopPad;
})
.attr("font-size", 11)
.attr("text-anchor", "start")
.attr("text-height", 14)
.attr("fill", function(d, i) {
return d3.rgb(theColorScale(i)).darker();
});
}
body {
background: #fff;
font-family: 'Open-Sans', sans-serif;
}
#container {
margin: 0 auto;
position: relative;
width: 800px;
overflow: visible;
}
.svg {
width: 800px;
height: 400px;
overflow: visible;
position: absolute;
}
.grid .tick {
stroke: lightgrey;
opacity: 0.3;
shape-rendering: crispEdges;
}
.grid path {
stroke-width: 0;
}
<div id="container">
<div class="svg"></div>
</div>
<script src="https://d3js.org/d3.v3.js"></script>
Related
I'm trying to create a kind of scatterplot with the following data.
array = [{
key: "6S",
values: {3: [{Id: "1234a"}, {Id: "1234b"}]}
},
{
key: "7S",
values: {5: [{Id: "1534a"}],4: [{Id: "1534a"}]}
}
]
The x axis represents the "key" value ("6S" and "7S" in the array) and the y axis the key from the values ("3", "5", "4"..). x is defined as scalBand and y as scaleLinear. If the key from the values has 2 objects (in our example "3" has 2 objects) I want to add 2 points side by side.
view1.selectAll("circle")
.data(array)
.enter()
.append("circle")
.attr("r", 2.5)
.attr("cx", function(d) { return x1(d.key); }) //must return something else
.attr("width", x1.bandwidth())
.attr("cy", function(d) { return y1(Object.keys(d.values))})
.attr("height", function(d) {return height-y1(Object.keys(d.values))});
The domain from x is:
x.domain(data.map(function(d) {
return d.key;
}));
from y:
y1.domain([0, 200]);
Any idea how I could return the x-axis position?
I was able to solve this by doing the following.
Change the structure of the data and create separate data points by splitting the values(the values key in your object) into a different array newArray.
Create an array of the cx and cy attributes of your circles to check if there are any overlaps. If yes, shift the cx of the overlapping circle by say 10px.
Here's the fiddle:
var svgWidth = 1300,
svgHeight = 600;
var width = 600,
height = 250;
var margin2 = {
top: 10,
right: 20,
bottom: 20,
left: 40
};
//Create SVG
var svg = d3.select("body")
.append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight);
var view1 = svg.append("g")
.attr("class", "view1")
.attr("transform", "translate(" + margin2.left + "," + margin2.top + ")");
var array = [{
key: "6S",
values: {
3: [{
Id: "1234a"
}, {
Id: "1234b"
}]
}
},
{
key: "7S",
values: {
5: [{
Id: "1534a"
}],
4: [{
Id: "1534a"
}]
}
}
];
var newArray = [];
array.forEach(function(d) {
if (Object.keys(d.values).length > 1) {
for (i in Object.keys(d.values)) {
var val = {}
val["key"] = d.key
val['values'] = {}
val['values'][Object.keys(d.values)[i]] = d.values[Object.keys(d.values)[i]]
newArray.push(val)
}
} else {
if (d.values[Object.keys(d.values)[0]].length > 1) {
for (i in d.values[Object.keys(d.values)[0]]) {
var val = {}
val["key"] = d.key
val['values'] = {}
val['values'][Object.keys(d.values)[0]] = [d.values[Object.keys(d.values)[0]][i]]
newArray.push(val)
}
} else {
newArray.push(d)
}
}
});
var cxList = [];
var x1 = d3.scaleBand().range([0, width]).padding(0.2),
y1 = d3.scaleLinear().range([height, 0]);
var xAxis1 = d3.axisBottom(x1),
yAxis1 = d3.axisLeft(y1);
x1.domain(newArray.map(function(d) {
return d.key;
}));
y1.domain([0, 10]);
//Add the "points"
view1.selectAll("circle")
.data(newArray)
.enter()
.append("circle")
.attr("class", "bar2")
.attr("r", 2.5)
.attr("cx", function(d) {
if (cxList.indexOf(x1(d.key) + '-' + y1(Object.keys(d.values))) > -1) {
cxList.push((x1(d.key) + 10) + '-' + y1(Object.keys(d.values)));
return x1(d.key) + 10;
} else {
cxList.push(x1(d.key) + '-' + y1(Object.keys(d.values)));
return x1(d.key);
}
})
.attr("width", x1.bandwidth())
.attr("cy", function(d) {
return y1(Object.keys(d.values))
})
.attr("height", function(d) {
return height - y1(Object.keys(d.values))
});
//Add x Axis & rotate text
view1.append("g")
.attr("transform", "translate(0," + height + ")")
.call(xAxis1)
.selectAll("text")
.attr("x", 2.5)
.attr("y", 5)
.attr("transform", "rotate(25)")
.style("text-anchor", "start");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<style>
body {
font: 11px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.dot {
stroke: #000;
}
.tooltip {
position: absolute;
width: 200px;
height: 28px;
pointer-events: none;
}
</style>
<body>
<svg width='500' height='500'></svg>
</body>
The code you provided still has some stuff missing. The circles and the axis look a little off in my code but should work fine in yours.
I'm quite new to HTML and JavaScript. I'm facing the famous Hierarchical Edge Bundling available here, which is generated by the D3.js library.
My goal is to add a semi-circular label zone in order to obtain something like this: every final node group is labelled with the parent's name.
Unfortunately, I have not found any code where I could take inspiration yet, except the code available in the link above: my idea would be to modify that code adding some line in order to generate the labels.
I saw this link with a snippet of code that may do to the trick, but I don't know how to use it (and whether I am in the right direction or not)
node.append("text")
.attr("dy", ".31em")
.attr("x", function(d) { return d.x < 180 === !d.children ? 6 : -6; })
.style("text-anchor", function(d) { return d.x < 180 === !d.children ? "start" : "end"; })
.attr("transform", function(d) { return "rotate(" + (d.x < 180 ? d.x - 90 : d.x + 90) + ")"; })
.text(function(d) { return d.id.substring(d.id.lastIndexOf(".") + 1); });
Does someone have any suggestion?
The basic idea is to draw a series of arcs around the links and shunt the labels outwards by the width of the arc.
V4 solution
A working adaption of the d3 v4 code from the linked block is below:
var flare = "https://gist.githubusercontent.com/robinmackenzie/d01d286d9ac16b474a2a43088c137d00/raw/c53c1eda18cc21636ae52dfffa3e030295916c98/flare.json";
d3.json(flare, function(err, json) {
if (err) throw err;
render(json);
});
function render(classes) {
// original code
var diameter = 960,
radius = diameter / 2,
innerRadius = radius - 120;
var cluster = d3.cluster()
.size([360, innerRadius]);
var line = d3.radialLine()
.curve(d3.curveBundle.beta(0.85))
.radius(function(d) { return d.y; })
.angle(function(d) { return d.x / 180 * Math.PI; });
var svg = d3.select("body").append("svg")
.attr("width", diameter)
.attr("height", diameter)
.append("g")
.attr("transform", "translate(" + radius + "," + radius + ")");
var link = svg.append("g").selectAll(".link"),
node = svg.append("g").selectAll(".node");
var root = packageHierarchy(classes)
.sum(function(d) { return d.size; });
cluster(root);
// added code -----
var arcInnerRadius = innerRadius;
var arcWidth = 30;
var arcOuterRadius = arcInnerRadius + arcWidth;
var arc = d3.arc()
.innerRadius(arcInnerRadius)
.outerRadius(arcOuterRadius)
.startAngle(function(d) { return d.st; })
.endAngle(function(d) { return d.et; });
var leafGroups = d3.nest()
.key(function(d) { return d.parent.data.name.split(".")[1]; })
.entries(root.leaves())
var arcAngles = leafGroups.map(function(group) {
return {
name: group.key,
min: d3.min(group.values, function(d) { return d.x }),
max: d3.max(group.values, function(d) { return d.x })
}
});
svg
.selectAll(".groupArc")
.data(arcAngles)
.enter()
.append("path")
.attr("id", function(d, i) { return`arc_${i}`; })
.attr("d", function(d) { return arc({ st: d.min * Math.PI / 180, et: d.max * Math.PI / 180}) }) // note use of arcWidth
.attr("fill", "steelblue");
svg
.selectAll(".arcLabel")
.data(arcAngles)
.enter()
.append("text")
.attr("x", 5) //Move the text from the start angle of the arc
.attr("dy", (d) => ((arcOuterRadius - arcInnerRadius) * 0.8)) //Move the text down
.append("textPath")
.attr("class", "arcLabel")
.attr("xlink:href", (d, i) => `#arc_${i}`)
.text((d, i) => d.name)
.style("font", `300 14px "Helvetica Neue", Helvetica, Arial, sans-serif`)
.style("fill", "#fff");
// ----------------
link = link
.data(packageImports(root.leaves()))
.enter().append("path")
.each(function(d) { d.source = d[0], d.target = d[d.length - 1]; })
.attr("class", "link")
.attr("d", line);
node = node
.data(root.leaves())
.enter().append("text")
.attr("class", "node")
.attr("dy", "0.31em")
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + (d.y + 8 + arcWidth) + ",0)" + (d.x < 180 ? "" : "rotate(180)"); })
.attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
.text(function(d) { return d.data.key; })
.on("mouseover", mouseovered)
.on("mouseout", mouseouted);
function mouseovered(d) {
node
.each(function(n) { n.target = n.source = false; });
link
.classed("link--target", function(l) { if (l.target === d) return l.source.source = true; })
.classed("link--source", function(l) { if (l.source === d) return l.target.target = true; })
.filter(function(l) { return l.target === d || l.source === d; })
.raise();
node
.classed("node--target", function(n) { return n.target; })
.classed("node--source", function(n) { return n.source; });
}
function mouseouted(d) {
link
.classed("link--target", false)
.classed("link--source", false);
node
.classed("node--target", false)
.classed("node--source", false);
}
// Lazily construct the package hierarchy from class names.
function packageHierarchy(classes) {
var map = {};
function find(name, data) {
var node = map[name], i;
if (!node) {
node = map[name] = data || {name: name, children: []};
if (name.length) {
node.parent = find(name.substring(0, i = name.lastIndexOf(".")));
node.parent.children.push(node);
node.key = name.substring(i + 1);
}
}
return node;
}
classes.forEach(function(d) {
find(d.name, d);
});
return d3.hierarchy(map[""]);
}
// Return a list of imports for the given array of nodes.
function packageImports(nodes) {
var map = {},
imports = [];
// Compute a map from name to node.
nodes.forEach(function(d) {
map[d.data.name] = d;
});
// For each import, construct a link from the source to target node.
nodes.forEach(function(d) {
if (d.data.imports) d.data.imports.forEach(function(i) {
imports.push(map[d.data.name].path(map[i]));
});
});
return imports;
}
}
.node {
font: 300 11px "Helvetica Neue", Helvetica, Arial, sans-serif;
fill: #bbb;
}
.node:hover {
fill: #000;
}
.link {
stroke: steelblue;
stroke-opacity: 0.4;
fill: none;
pointer-events: none;
}
.node:hover,
.node--source,
.node--target {
font-weight: 700;
}
.node--source {
fill: #2ca02c;
}
.node--target {
fill: #d62728;
}
.link--source,
.link--target {
stroke-opacity: 1;
stroke-width: 2px;
}
.link--source {
stroke: #d62728;
}
.link--target {
stroke: #2ca02c;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
You can see the discrete block of code added to draw and label the arcs - the key bit to calculate the start and end angles for the arc generator are:
var leafGroups = d3.nest()
.key(function(d) { return d.parent.data.name.split(".")[1]; })
.entries(root.leaves())
var arcAngles = leafGroups.map(function(group) {
return {
name: group.key,
min: d3.min(group.values, function(d) { return d.x }),
max: d3.max(group.values, function(d) { return d.x })
}
});
For leafGroups, the nest function is grouping the leaves of the hierarchy by the second item of the key e.g. flare.analytics.cluster = analytics and flare.vis.operator.distortion = vis. There is a choice here for different data sets that you need to have a think about e.g. if the leaves are always at a consistent depth; are the labels always unique. Defining the 'parent group' can either be a top-down or bottom-up definition.
For arcAngles, you just need the min and max of each group then you can go ahead and draw the arcs and label them. I lifted some of the labelling from here which is a great article on labelling arcs in d3. You need to have a think, again, for this bit because if the label is too long for the arc it doesn't look great - see the "Display" label in the example.
The other change is further down here:
node = node
.data(root.leaves())
.enter().append("text")
.attr("class", "node")
.attr("dy", "0.31em")
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + (d.y + 8 + arcWidth) + ",0)" + (d.x < 180 ? "" : "rotate(180)"); })
.attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
.text(function(d) { return d.data.key; })
.on("mouseover", mouseovered)
.on("mouseout", mouseouted);
Noting you need to add arcWidth when setting the transform attribute - this moves the node labels outward to accommodate the arcs.
V6+ solution
There's a newer version of hierarchical edge bundling in this Observable HQ page using d3 v6 (and also by Mike Bostok). We can add similar code to identify groups, get the min/ max of the angles and push the labels outward a bit to accomodate the arcs.
const flare = "https://gist.githubusercontent.com/robinmackenzie/d01d286d9ac16b474a2a43088c137d00/raw/c53c1eda18cc21636ae52dfffa3e030295916c98/flare.json";
const colorin = "#00f";
const colorout = "#f00";
const colornone = "#ccc";
const width = 960;
const radius = width / 2;
d3.json(flare).then(json => render(json));
function render(data) {
const line = d3.lineRadial()
.curve(d3.curveBundle.beta(0.85))
.radius(d => d.y)
.angle(d => d.x);
const tree = d3.cluster()
.size([2 * Math.PI, radius - 100]);
const root = tree(bilink(d3.hierarchy(hierarchy(data))
.sort((a, b) => d3.ascending(a.height, b.height) || d3.ascending(a.data.name, b.data.name))));
const svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", width)
.append("g")
.attr("transform", `translate(${radius},${radius})`);
// NEW CODE BELOW ----------------------------------------
// add arcs with labels
const arcInnerRadius = radius - 100;
const arcWidth = 30;
const arcOuterRadius = arcInnerRadius + arcWidth;
const arc = d3
.arc()
.innerRadius(arcInnerRadius)
.outerRadius(arcOuterRadius)
.startAngle((d) => d.start)
.endAngle((d) => d.end);
const leafGroups = d3.groups(root.leaves(), d => d.parent.data.name);
const arcAngles = leafGroups.map(g => ({
name: g[0],
start: d3.min(g[1], d => d.x),
end: d3.max(g[1], d => d.x)
}));
svg
.selectAll(".arc")
.data(arcAngles)
.enter()
.append("path")
.attr("id", (d, i) => `arc_${i}`)
.attr("d", (d) => arc({start: d.start, end: d.end}))
.attr("fill", "blue")
.attr("stroke", "blue");
svg
.selectAll(".arcLabel")
.data(arcAngles)
.enter()
.append("text")
.attr("x", 5) //Move the text from the start angle of the arc
.attr("dy", (d) => ((arcOuterRadius - arcInnerRadius) * 0.8)) //Move the text down
.append("textPath")
.attr("class", "arcLabel")
.attr("xlink:href", (d, i) => `#arc_${i}`)
.text((d, i) => ((d.end - d.start) < (6 * Math.PI / 180)) ? "" : d.name); // 6 degrees min arc length for label to apply
// --------------------------------------------------------
// add nodes
const node = svg.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.selectAll("g")
.data(root.leaves())
.join("g")
.attr("transform", d => `rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y}, 0)`)
.append("text")
.attr("dy", "0.31em")
.attr("x", d => d.x < Math.PI ? (arcWidth + 5) : (arcWidth + 5) * -1) // note use of arcWidth
.attr("text-anchor", d => d.x < Math.PI ? "start" : "end")
.attr("transform", d => d.x >= Math.PI ? "rotate(180)" : null)
.text(d => d.data.name)
.each(function(d) { d.text = this; })
.on("mouseover", overed)
.on("mouseout", outed)
.call(text => text.append("title").text(d => `${id(d)} ${d.outgoing.length} outgoing ${d.incoming.length} incoming`));
// add edges
const link = svg.append("g")
.attr("stroke", colornone)
.attr("fill", "none")
.selectAll("path")
.data(root.leaves().flatMap(leaf => leaf.outgoing))
.join("path")
.style("mix-blend-mode", "multiply")
.attr("d", ([i, o]) => line(i.path(o)))
.each(function(d) { d.path = this; });
function overed(event, d) {
link.style("mix-blend-mode", null);
d3.select(this).attr("font-weight", "bold");
d3.selectAll(d.incoming.map(d => d.path)).attr("stroke", colorin).raise();
d3.selectAll(d.incoming.map(([d]) => d.text)).attr("fill", colorin).attr("font-weight", "bold");
d3.selectAll(d.outgoing.map(d => d.path)).attr("stroke", colorout).raise();
d3.selectAll(d.outgoing.map(([, d]) => d.text)).attr("fill", colorout).attr("font-weight", "bold");
}
function outed(event, d) {
link.style("mix-blend-mode", "multiply");
d3.select(this).attr("font-weight", null);
d3.selectAll(d.incoming.map(d => d.path)).attr("stroke", null);
d3.selectAll(d.incoming.map(([d]) => d.text)).attr("fill", null).attr("font-weight", null);
d3.selectAll(d.outgoing.map(d => d.path)).attr("stroke", null);
d3.selectAll(d.outgoing.map(([, d]) => d.text)).attr("fill", null).attr("font-weight", null);
}
function id(node) {
return `${node.parent ? id(node.parent) + "." : ""}${node.data.name}`;
}
function bilink(root) {
const map = new Map(root.leaves().map(d => [id(d), d]));
for (const d of root.leaves()) d.incoming = [], d.outgoing = d.data.imports.map(i => [d, map.get(i)]);
for (const d of root.leaves()) for (const o of d.outgoing) o[1].incoming.push(o);
return root;
}
function hierarchy(data, delimiter = ".") {
let root;
const map = new Map;
data.forEach(function find(data) {
const {name} = data;
if (map.has(name)) return map.get(name);
const i = name.lastIndexOf(delimiter);
map.set(name, data);
if (i >= 0) {
find({name: name.substring(0, i), children: []}).children.push(data);
data.name = name.substring(i + 1);
} else {
root = data;
}
return data;
});
return root;
}
}
.node {
font: 300 11px "Helvetica Neue", Helvetica, Arial, sans-serif;
fill: #fff;
}
.arcLabel {
font: 300 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
fill: #fff;
}
.node:hover {
fill: #000;
}
.link {
stroke: steelblue;
stroke-opacity: 0.4;
fill: none;
pointer-events: none;
}
.node:hover,
.node--source,
.node--target {
font-weight: 700;
}
.node--source {
fill: #2ca02c;
}
.node--target {
fill: #d62728;
}
.link--source,
.link--target {
stroke-opacity: 1;
stroke-width: 2px;
}
.link--source {
stroke: #d62728;
}
.link--target {
stroke: #2ca02c;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.0.0/d3.min.js"></script>
Some differences to note:
The hierarchy function differs from the packageHierarchy function in the original block - seemingly we no longer have the full path of the hierarchy and therefore there's ambiguity for flare.vis.data.EdgeSprite vs flare.data.DataField i.e. two leaves can have the same 'parent' in different branches of the hierarchy.
I've fixed the input to accomodate that but it changes how the 'parent group' is identified i.e. bottom-up vs top-down in the original.
nest has gone so you can use groups instead
The v4 seems to have objects defined with angles in degrees, but in v6 they are in radians - so you will see a few * Math.PI / 180 in the v4 version and not in the v6 - but it's just degrees/ radians conversion.
for long labels, I use a threshold such that an arc has to be minimum 6 degrees long otherwise the label won't place (.text((d, i) => ((d.end - d.start) < (6 * Math.PI / 180)) ? "" : d.name);)
var margin = {top: 20, right: 30, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom,
padding = 0.3;
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], padding);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickFormat(function(d) { return percentage(d); })
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0)
.attr("align","middle");
var chart = d3.select(".chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
data = [
{"name":"Product Revenue","value":420000},
{"name":"Services Revenue","value":210000},
{"name":"Employee Revenue","value":190000},
{"name":"Fixed Costs","value":-170000},
{"name":"Variable Costs","value":-140000}
];
//function to find all the positive values
var positive_val = data.filter(function(d) { return d.value > 0; });
console.log(JSON.stringify(positive_val));
//function to calculate the sum of all the positive values
var maxSum = positive_val.reduce(function(sum, d) {
return sum + d.value;
}, 0);
console.log("The maximum sum is "+maxSum);
//to calculate the new Domain by adding 120
var yaxisRange=maxSum+120;
console.log("The y axis sum is "+yaxisRange);
var newDomain=percentage(yaxisRange);
console.log(newDomain);
var newDomain = newDomain.replace(/[!##$%^&*]/g, "");
console.log(newDomain);
// Transform data (i.e., finding cumulative values and total)
var cumulative = 0;
for (var i = 0; i < data.length; i++) {
data[i].start = cumulative;
cumulative += data[i].value;
data[i].end = cumulative;
data[i].class = ( data[i].value >= 0 ) ? 'positive' : 'negative'
}
data.push({
name: 'Total',
end: cumulative,
start: 0,
class: 'total',
value: cumulative
});
x.domain(data.map(function(d) { return d.name; }));
y.domain([0, d3.max(data, function(d) { return d.end; })]);
//WHen i try to use this as my new domain,the bar increase the height
//y.domain([0,newDomain]);
debugger;
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
chart.append("g")
.attr("class", "y axis")
.call(yAxis);
var bar = chart.selectAll(".bar")
.data(data)
.enter().append("g")
.attr("class", function(d) { return "bar " + d.class })
.attr("transform", function(d) { return "translate(" + x(d.name) + ",0)"; });
bar.append("rect")
//.attr("y", function(d) { return y(d.value); })
.attr("y", function(d) { return y( Math.max(d.start, d.end) ); })
.attr("height", function(d) { return Math.abs( y(d.start) - y(d.end) ); })
//function to draw the tooltip
.attr("width", x.rangeBand()).on("mouseover", function(d) {
// to find the parent node,to calculate the x position
var parentG = d3.select(this.parentNode);
var barPos = parseFloat(parentG.attr('transform').split("(")[1]);
var xPosition = barPos+x.rangeBand()/2;
//to find the y position
var yPosition = parseFloat(d3.select(this).attr("y"))+ Math.abs( y(d.start) - y(d.end))/2;
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html(d.name + "<br/>" + percentage(d.value))
.style("left", xPosition + "px")
.style("top", yPosition + "px");
}).on("mouseout", function(d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
});
bar.append("text")
.attr("x", x.rangeBand() / 2)
.attr("y", function(d) { return y(d.end) + 5; })
.attr("dy", function(d) { return ((d.class=='negative') ? '-' : '') + ".75em" })
.text(function(d) { return percentage(d.end - d.start);});
bar.filter(function(d) { return d.class != "total" }).append("line")
.attr("class", "connector")
.attr("x1", x.rangeBand() + 5 )
.attr("y1", function(d) { return y(d.end) } )
.attr("x2", x.rangeBand() / ( 1 - padding) - 5 )
.attr("y2", function(d) { return y(d.end) } )
function type(d) {
d.value = +d.value;
return d;
}
function percentage(n) {
n = Math.round(n);
var result = n;
if (Math.abs(n) > 100) {
result = Math.round(n/100) + '%';
}
return result;
}
-Here is the updated fiddle http://jsfiddle.net/7mkq4k8k/21/
-I want to make the yaxis label increase .for eg 9000,9500.I have calculated the newDomian.
-If i try to add this domain,my chart doesnt get drawn properly.The height of the bars increase ,and the due to this the rest of the bars are not drawn.Please help me in this issue.
So the chart you initially draw is based on this domain :
y.domain([0, d3.max(data, function (d) {
return d.end;
})]);
Try to console.log(d3.max(data, function (d) {return d.end;})) and you will find it returns 820000, which is the maximum of your cumulative calculation. That means your chart is drawn with a domain from 0 to 820000.
Now let's talk about your newDomain. You're taking the percentage of your maxSum, which means your newDomain is equal to 8201. So now you're trying to draw your chart from 0 to 8201.
But your bars height is calculated like this :
Math.abs(y(d.start) - y(d.end)), which means you are calculating ranges from y(0) to y(820000) (d.end is max equal to 820000).
y(820000) doesn't fit, as you specified with your domain that it could max go to y(8201). That's why your bars are reaching over the very top of your chart, because the domain you're giving doesn't correspond the numbers inside :
y(this number is too big and doesn't fit because it is not between 0 and newDomain).
How to solve this ?
You define your domain correctly, removing the percentage line
//function to calculate the sum of all the positive values
var maxSum = positive_val.reduce(function (sum, d) {
return sum + d.value;
}, 0);
console.log("The maximum sum is " + maxSum);
//to calculate the new Domain by adding 520000 (big number to show you it works)
var newDomain = maxSum + 520000;
console.log(newDomain); //1340000
y.domain([0,newDomain]);
Working snippet below :
var margin = {
top: 20,
right: 30,
bottom: 30,
left: 40
},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom,
padding = 0.3;
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], padding);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickFormat(function (d) {
return percentage(d);
})
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0)
.attr("align", "middle");
var chart = d3.select(".chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
data = [{
"name": "Product Revenue",
"value": 420000
}, {
"name": "Services Revenue",
"value": 210000
}, {
"name": "Employee Revenue",
"value": 190000
}, {
"name": "Fixed Costs",
"value": -170000
}, {
"name": "Variable Costs",
"value": -140000
}
];
//function to find all the positive values
var positive_val = data.filter(function (d) {
return d.value > 0;
});
console.log(JSON.stringify(positive_val));
//function to calculate the sum of all the positive values
var maxSum = positive_val.reduce(function (sum, d) {
return sum + d.value;
}, 0);
console.log("The maximum sum is " + maxSum);
//to calculate the new Domain by adding 120
var newDomain = maxSum + 520000;
console.log(newDomain);
// Transform data (i.e., finding cumulative values and total)
var cumulative = 0;
for (var i = 0; i < data.length; i++) {
data[i].start = cumulative;
cumulative += data[i].value;
data[i].end = cumulative;
data[i].class = (data[i].value >= 0) ? 'positive' : 'negative'
}
data.push({
name: 'Total',
end: cumulative,
start: 0,
class: 'total',
value: cumulative
});
x.domain(data.map(function (d) {
return d.name;
}));
console.log(d3.max(data, function (d) {
return d.end;
}));
/*y.domain([0, d3.max(data, function (d) {
return d.end;
})]);*/
//WHen i try to use this as my new domain,the bar increase the height
y.domain([0,newDomain]);
debugger;
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
chart.append("g")
.attr("class", "y axis")
.call(yAxis);
var bar = chart.selectAll(".bar")
.data(data)
.enter().append("g")
.attr("class", function (d) {
return "bar " + d.class
})
.attr("transform", function (d) {
return "translate(" + x(d.name) + ",0)";
});
bar.append("rect")
//.attr("y", function(d) { return y(d.value); })
.attr("y", function (d) {
return y(Math.max(d.start, d.end));
})
.attr("height", function (d) {
return Math.abs(y(d.start) - y(d.end));
})
//function to draw the tooltip
.attr("width", x.rangeBand()).on("mouseover", function (d) {
// to find the parent node,to calculate the x position
var parentG = d3.select(this.parentNode);
var barPos = parseFloat(parentG.attr('transform').split("(")[1]);
var xPosition = barPos + x.rangeBand() / 2;
//to find the y position
var yPosition = parseFloat(d3.select(this).attr("y")) + Math.abs(y(d.start) - y(d.end)) / 2;
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html(d.name + "<br/>" + percentage(d.value))
.style("left", xPosition + "px")
.style("top", yPosition + "px");
}).on("mouseout", function (d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
});
bar.append("text")
.attr("x", x.rangeBand() / 2)
.attr("y", function (d) {
return y(d.end) + 5;
})
.attr("dy", function (d) {
return ((d.class == 'negative') ? '-' : '') + ".75em"
})
.text(function (d) {
return percentage(d.end - d.start);
});
bar.filter(function (d) {
return d.class != "total"
}).append("line")
.attr("class", "connector")
.attr("x1", x.rangeBand() + 5)
.attr("y1", function (d) {
return y(d.end)
})
.attr("x2", x.rangeBand() / (1 - padding) - 5)
.attr("y2", function (d) {
return y(d.end)
})
function type(d) {
d.value = +d.value;
return d;
}
function percentage(n) {
n = Math.round(n);
var result = n;
if (Math.abs(n) > 100) {
result = Math.round(n / 100) + '%';
}
return result;
}
.bar.total rect {
fill: steelblue;
}
.bar:hover rect {
fill:orange;
}
.bar.positive rect {
fill: darkolivegreen;
}
.bar:hover rect {
fill:orange;
}
.bar.negative rect {
fill: crimson;
}
.bar:hover rect {
fill:orange;
}
.bar line.connector {
stroke: grey;
stroke-dasharray: 3;
}
.bar text {
fill: white;
font: 12px sans-serif;
text-anchor: middle;
}
.axis text {
font: 10px sans-serif;
}
.axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
div.tooltip {
position:absolute;
text-align: center;
padding: 2px;
font: 12px sans-serif;
background: #33CC00;
border: 0px;
border-radius: 8px;
pointer-events: none;
width: 90px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg class="chart"></svg>
Hope this helps !
I have made a bar chart based on revenue(x axis) and country(y axis) basis.My bar chart is done but I want to select multiple values of country from a dropdown and according to those values the bars of only that country should be shown others hide...For single selection i have done but for multiple values of country how to filter and show in D3 Js bar chart I am stuck.
The localDataJson contains the data:
localdatajson=[
{"Country";"USA","Revenue":"12","TurnOver":"16"},
{"Country";"Brazil","Revenue":"4.5","TurnOver":"16"},
{"Country";"Belzium","Revenue":"4.8","TurnOver":"16"},
{"Country";"Britain","Revenue":"20","TurnOver":"16"},
{"Country";"Canada","Revenue":"6.5","TurnOver":"16"},
{"Country";"DenMark","Revenue":"7.5","TurnOver":"16"}
]
text parameter would be an array in case of multiple selection like
for eg. text=["USA","Brazil","Britain"]
I want to show bars only for these three countries...
Here is my code
function revenueBar(localDataJson, text) {
var w = 400;
var h = 400;
var barPadding = 1;
var maxRevenue = 0;
var maxTurnOver = 0;
var padding = {
left: 45, right: 10,
top: 40, bottom: 60
}
var maxWidth = w - padding.left - padding.right;
var maxHeight = h - padding.top - padding.bottom;
for (var j = 0; j < localDataJson.length; j++) {
if (localDataJson[j].Revenue > maxRevenue) {
maxRevenue = localDataJson[j].Revenue;
}
}
for (var j = 0; j < localDataJson.length; j++) {
if (localDataJson[j].TurnOver > maxTurnOver) {
maxTurnOver = localDataJson[j].TurnOver;
}
}
var convert = {
x: d3.scale.ordinal(),
y: d3.scale.linear()
};
// Define your axis
var axis = {
x: d3.svg.axis().orient('bottom')
//y: d3.svg.axis().orient('left')
};
// Define the conversion function for the axis points
axis.x.scale(convert.x);
// axis.y.scale(convert.y);
// Define the output range of your conversion functions
convert.y.range([maxHeight, 0]);
convert.x.rangeRoundBands([0, maxWidth]);
convert.x.domain(localDataJson.map(function (d) {
return d.Country;
})
);
convert.y.domain([0, maxRevenue]);
$('#chartBar').html("");
var svg = d3.select("#chartBar")
.append("svg")
.attr("width", w)
.attr("height", h);
// The group node that will contain all the other nodes
// that render your chart
$('.bar-group').html("");
var chart = svg.append('g')
.attr({
class: 'container',
transform: function (d, i) {
return 'translate(' + padding.left + ',' + padding.top + ')';
}
});
chart.append('g') // Container for the axis
.attr({
class: 'x axis',
transform: 'translate(0,' + maxHeight + ')'
})
.call(axis.x)
.selectAll("text")
.attr("x", "-.8em")
.attr("y", ".15em")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)");// Insert an axis inside this node
$('.axis path').css("fill", "none");
chart.append('g') // Container for the axis
// .attr({
// class: 'y axis',
// height: maxHeight,
// })
//.call(axis.y);
var bars = chart
.selectAll('g.bar-group')
.data(localDataJson)
.enter()
.append('g') // Container for the each bar
.attr({
transform: function (d, i) {
return 'translate(' + convert.x(d.Country) + ', 1)';
},
class: 'bar-group'
});
//Here goes filter thing ,bar of filter values will be shown others hide
if (text != "All" && text != "Clear Filter") {
svg.selectAll('g.bar-group')
.filter(function (d) {
return text != d.Country;
})
.attr("display", "none");
svg.selectAll('g.bar-group')
.filter(function (d) {
return text == d.Country;
})
.attr("display", "inline");
}
var color = d3.scale.category20();
// var color = d3.scale.ordinal()
// .range(['#f1595f', '#79c36a', '#599ad3', '#f9a65a', '#9e66ab','#cd7058']);
bars.append('rect')
.attr({
y: maxHeight,
height: 0,
width: function (d) { return convert.x.rangeBand(d) - 3; },
class: 'bar'
})
.transition()
.duration(1500)
.attr({
y: function (d, i) {
return convert.y(d.Revenue);
},
height: function (d, i) {
return maxHeight - convert.y(d.Revenue);
}
})
.attr("fill", function (d, i) {
// return color(i);
return color(d.Country);
})
// for (var i = 0; i < text.length; i++) {
// }
svg.append("text")
.attr("x", (w + padding.left + padding.right) / 2)
.attr("y", 25)
.attr("class", "title")
.attr("text-anchor", "middle")
.text("Revenue Bar Chart")
;
var svgs = svg.select("g.container").selectAll("text.label")
// svgs.selectAll("text")
.data(localDataJson)
.enter()
.append("text")
.classed("label", true)
//.transition() // <-- This is new,
// .duration(5000)
.text(function (d) {
return (d.Revenue);
})
.attr("text-anchor", "middle")
//// Set x position to the left edge of each bar plus half the bar width
.attr("x", function (d, i) {
// return (i * (w / localDataJson.length)) + ((w / localDataJson.length - barPadding) / 2);
return convert.x(d.Country) + (convert.x.rangeBand(d) - 3) / 2;
})
.attr({
y: function (d, i) {
return convert.y(d.Revenue) +20;
// return maxHeight;
},
})
.attr("font-family", "sans-serif")
.attr("font-size", "13px")
.attr("fill", "white")
}
Garima you need to do it like this in the section where you hiding the bars using display:none
svg.selectAll('g.bar-group')
.filter(function (d) {
if(text.indexOf(d.Country) == -1)
return true;
else
return false;
})
.attr("display", "none");
Note: In this i am assuming that text is an array of selected countries even when its a single selection.
Hope this helps
The code below displays marker-ends on arrows/paths/lines as intended, but the color of the marker-end does not vary by line (i.e., it is always the same orange color, not the color of its respective line). I think the code is defaulting to the color assigned to the first field of my data(?). Any advice would be appreciated.
<script src="http://www.protobi.com/javascripts/d3.v3.min.js"></script>
<script src="http://labratrevenge.com/d3-tip/javascripts/d3.tip.v0.6.3.js"></script>
<script src="http://www.protobi.com/examples/pca/pca.js"></script>
<script type="text/javascript">
var margin = {top: 20, right: 20, bottom: 20, left: 20};
var width = 1500 - margin.left - margin.right;
var height = 1500 - margin.top - margin.bottom;
var angle = Math.PI * 0;
var color = d3.scale.category10();
var x = d3.scale.linear().range([width, 0]); // switch to match how R biplot shows it
var y = d3.scale.linear().range([height, 0]);
x.domain([-3.5,3.5]).nice()
y.domain([-3.5,3.5]).nice()
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var svg = d3.select("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("/brand.csv", function(error, data) {
var matrix = data.map(function(d){
return d3.values(d).slice(1,d.length).map(parseFloat);
});
var pca = new PCA();
matrix = pca.scale(matrix,true,true);
pc = pca.pca(matrix,2)
var A = pc[0]; // this is the U matrix from SVD
var B = pc[1]; // this is the dV matrix from SVD
var brand_names = Object.keys(data[0]); // first row of data file ["ATTRIBUTE", "BRAND A", "BRAND B", "BRAND C", ...]
brand_names.shift(); // drop the first column label, e.g. "ATTRIBUTE"
data.map(function(d,i){
label: d.ATTRIBUTE,
d.pc1 = A[i][0];
d.pc2 = A[i][1];
});
var label_offset = {
"Early/First line": 20,
"Unfamiliar":10,
"Convenient": -5
}
var brands = brand_names
.map(function(key, i) {
return {
brand: key,
pc1: B[i][0]*4,
pc2: B[i][1]*4
}
});
function rotate(x,y, dtheta) {
var r = Math.sqrt(x*x + y*y);
var theta = Math.atan(y/x);
if (x<0) theta += Math.PI;
return {
x: r * Math.cos(theta + dtheta),
y: r * Math.sin(theta + dtheta)
}
}
data.map(function(d) {
var xy = rotate(d.pc1, d.pc2, angle);
d.pc1 = xy.x;
d.pc2 = xy.y;
});
brands.map(function(d) {
var xy = rotate(d.pc1, d.pc2, angle);
d.pc1 = xy.x;
d.pc2 = xy.y;
});
var showAxis = false; // normally we don't want to see the axis in PCA, it's meaningless
if (showAxis) {
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.append("text")
.attr("class", "label")
.attr("x", width)
.attr("y", -6)
.style("text-anchor", "end")
.text("PC1");
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("PC2");
}
var legend = svg.selectAll(".legend")
.data(color.domain())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", 10.5)
.attr("cx", function(d) { return x(d.pc1); })
.attr("cy", function(d) { return y(d.pc2); })
.style("fill", function(d) { return color(d['species']); })
.on('mouseover', onMouseOverAttribute)
.on('mouseleave', onMouseLeave);
svg.selectAll("text.brand")
.data(brands)
.enter().append("text")
.attr("class", "label-brand")
.attr("x", function(d) { return x(d.pc1) + 10; })
.attr("y", function(d) { return y(d.pc2) + 0; })
.text(function(d) { return d['brand']})
svg.selectAll("marker.brand")
.data(brands)
.enter().append("svg:marker")
.attr('id', 'end-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 6)
.attr('markerWidth', 10)
.attr('markerHeight', 10)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5')
.style("fill", function(d) { return color(d['brand']); });
svg.selectAll(".line")
.data(brands)
.enter().append("line")
.attr("class", "square")
.attr('x1', function(d) { return x(-d.pc1);})
.attr('y1', function(d) { return y(-d.pc2); })
.attr("x2", function(d) { return x(d.pc1); })
.attr("y2", function(d) { return y(d.pc2); })
.style("stroke", function(d) { return color(d['brand']); })
.style('marker-end', "url(#end-arrow)")
.on('mouseover', onMouseOverBrand)
.on('mouseleave', onMouseLeave);
svg.selectAll("text.attr")
.data(data)
.enter().append("text")
.attr("class", "label-attr")
.attr("x", function(d,i ) { return x(d.pc1)+4 ; })
.attr("y", function(d ,i) { return y(d.pc2) + (label_offset[d.ATTRIBUTE]||0); })
.text(function(d,i) { return d.ATTRIBUTE})
var pctFmt = d3.format('0%')
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([10, 20])
.direction('e')
.html(function(values,title) {
var str =''
str += '<h3>' + (title.length==1 ? 'Brand ' : '' )+ title + '</h3>'
str += "<table>";
for (var i=0; i<values.length; i++) {
if (values[i].key != 'ATTRIBUTE' && values[i].key != 'pc1' && values[i].key != 'pc2') {
str += "<tr>";
str += "<td>" + values[i].key + "</td>";
str += "<td class=pct>" + pctFmt(values[i].value) + "</td>";
str + "</tr>";
}
}
str += "</table>";
return str;
});
svg.call(tip);
function getSpPoint(A,B,C){
var x1=A.x, y1=A.y, x2=B.x, y2=B.y, x3=C.x, y3=C.y;
var px = x2-x1, py = y2-y1, dAB = px*px + py*py;
var u = ((x3 - x1) * px + (y3 - y1) * py) / dAB;
var x = x1 + u * px, y = y1 + u * py;
return {x:x, y:y}; //this is D
}
// draw line from the attribute a perpendicular to each brand b
function onMouseOverAttribute(a,j) {
brands.forEach(function(b, idx) {
var A = { x: 0, y:0 };
var B = { x: b.pc1, y: b.pc2 };
var C = { x: a.pc1, y: a.pc2 };
b.D = getSpPoint(A,B,C);
});
svg.selectAll('.tracer')
.data(brands)
.enter()
.append('line')
.attr('class', 'tracer')
.attr('x1', function(b,i) { return x(a.pc1); return x1; })
.attr('y1', function(b,i) { return y(a.pc2); return y1; })
.attr('x2', function(b,i) { return x(b.D.x); return x2; })
.attr('y2', function(b,i) { return y(b.D.y); return y2; })
.style("stroke", function(d) { return "#aaa"});
delete a.D;
var tipText = d3.entries(a);
tip.show(tipText, a.ATTRIBUTE);
};
// draw line from the brand axis a perpendicular to each attribute b
function onMouseOverBrand(b,j) {
data.forEach(function(a, idx) {
var A = { x: 0, y:0 };
var B = { x: b.pc1, y: b.pc2 };
var C = { x: a.pc1, y: a.pc2 };
a.D = getSpPoint(A,B,C);
});
svg.selectAll('.tracer')
.data(data)
.enter()
.append('line')
.attr('class', 'tracer')
.attr('x1', function(a,i) { return x(a.D.x); })
.attr('y1', function(a,i) { return y(a.D.y); })
.attr('x2', function(a,i) { return x(a.pc1); })
.attr('y2', function(a,i) { return y(a.pc2); })
.style("stroke", function(d) { return "#aaa"});
var tipText = data.map(function(d) {
return {key: d.ATTRIBUTE, value: d[b['brand']] }
})
tip.show(tipText, b.brand);
};
function onMouseLeave(b,j) {
svg.selectAll('.tracer').remove()
tip.hide();
}
});
While you are creating an svg:marker for each line, you give them all the same id. When they are then used on your line elements, since they all have the same id you are only using one of them.
Simple fix, give them unique ids:
svg.selectAll("marker.brand")
.data(brands)
.enter().append("svg:marker")
.attr('id', function(d,i){
return 'end-arrow' + i; //<-- append index postion
})
...
svg.selectAll(".line")
.data(brands)
.enter().append("line")
.attr("class", "square")
.attr('x1', function(d) {
return x(-d.pc1);
})
.attr('y1', function(d) {
return y(-d.pc2);
})
.attr("x2", function(d) {
return x(d.pc1);
})
.attr("y2", function(d) {
return y(d.pc2);
})
.style("stroke", function(d) {
return color(d['brand']);
})
.style('marker-end', function(d,i){
return "url(#end-arrow"+i+")"; //<--use the one with the right id
})
....
Example here.