I was able to create my first htmlwidget that creates this animated plot:
I would like to replace the "B" and "D" buttons with a single icon that uses an svg as the icon. In particular, I want to use this icon.. The icon should be black when selected, light gray when unselected, and a darker gray when hovering over.
To start, I'm not sure where to save the file so my code can see it.
This is the yaml for my htmlwidget package:
# (uncomment to add a dependency)
dependencies:
- name: D3
version: 4
src: htmlwidgets/lib/D3
script: d3.v4.js
stylesheet: style.css
- name: d3tip
version: 0.7.1
src: htmlwidgets/lib/d3-tip
script: d3-tip.min.js
stylesheet: style.css
And this is the js file:
HTMLWidgets.widget({
name: 'IMPosterior',
type: 'output',
factory: function(el, width, height) {
// TODO: define shared variables for this instance
return {
renderValue: function(opts) {
//transition
var transDuration = 1000;
var dataDiscrete = opts.bars.map((b, i) => {
b.y = Number(b.y);
b.desc = opts.text[i];
return b;
});
var distParams = {
min: d3.min(opts.data, d => d.x),
max: d3.max(opts.data, d => d.x)
};
distParams.cuts = [-opts.MME, opts.MME, distParams.max];
opts.data = opts.data.sort((a,b) => a.x - b.x);
var dataContinuousGroups = [];
distParams.cuts.forEach((c, i) => {
let data = opts.data.filter(d => {
if (i === 0) {
return d.x < c;
} else if (i === distParams.cuts.length - 1) {
return d.x > distParams.cuts[i - 1];
} else {
return d.x < c && d.x > distParams.cuts[i - 1];
}
});
data.unshift({x:data[0].x, y:0});
data.push({x:data[data.length - 1].x, y:0});
dataContinuousGroups.push({
color: opts.colors[i],
data: data
});
});
var margin = {
top: 50,
right: 20,
bottom: 80,
left: 70
},
dims = {
width: width - margin.left - margin.right,
height: height - margin.top - margin.bottom
};
var xContinuous = d3.scaleLinear()
.domain([distParams.min - 1, distParams.max + 1])
.range([0, dims.width]);
var xDiscrete = d3.scaleBand()
.domain(dataDiscrete.map(function(d) { return d.x; }))
.rangeRound([0, dims.width]).padding(0.1);
var y = d3.scaleLinear()
.domain([0, 1])
.range([dims.height, 0]);
var svg = d3.select(el).append("svg")
.attr("width", dims.width + margin.left + margin.right)
.attr("height", dims.height + margin.top + margin.bottom);
var g = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var xAxis = d3.axisBottom()
.scale(xDiscrete);
var yAxis = d3.axisLeft()
.scale(y)
.ticks(10)
.tickFormat(d3.format(".0%"));
var yLabel = g.append("text")
.attr("class", "y-axis-label")
.attr("transform", "rotate(-90)")
.attr("y", -52)
.attr("x", -160)
.attr("dy", ".71em")
.style("text-anchor", "end")
.style("font-size", 14 + "px")
.text("Probability");
g.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + dims.height + ")")
.call(xAxis);
g.append("g")
.attr("class", "y axis")
.call(yAxis);
var areas = g.selectAll(".area")
.data(dataDiscrete)
.enter().append("path")
.attr("class", "area")
.style("fill", function(d) { return d.color; })
.attr("d", function(d, i) {
let numPts = dataContinuousGroups[i].data.length - 2;
var path = d3.path()
path.moveTo(xDiscrete(d.x), y(0));
for (j=0; j<numPts; j++) {
path.lineTo(xDiscrete(d.x) + j*xDiscrete.bandwidth()/(numPts-1), y(d.y))
}
path.lineTo(xDiscrete(d.x) + xDiscrete.bandwidth(), y(0));
return path.toString();
});
var tooltip = d3.tip()
.attr('class', 'd3-tip chart-data-tip')
.offset([30, 0])
.direction('s')
.html(function(d, i) {
return "<span>" + dataDiscrete[i].desc + "</span>";
});
g.call(tooltip);
areas
.on('mouseover', tooltip.show)
.on('mouseout', tooltip.hide);
var thresholdLine = g.append("line")
.attr("stroke", "black")
.style("stroke-width", "1.5px")
.style("stroke-dasharray", "5,5")
.style("opacity", 1)
.attr("x1", 0)
.attr("y1", y(opts.threshold))
.attr("x2", dims.width)
.attr("y2", y(opts.threshold));
var updateXAxis = function(type, duration) {
if (type === "continuous") {
xAxis.scale(xContinuous);
} else {
xAxis.scale(xDiscrete);
}
d3.select(".x").transition().duration(duration).call(xAxis);
};
var updateYAxis = function(data, duration) {
var extent = d3.extent(data, function(d) {
return d.y;
});
extent[0] = 0;
extent[1] = extent[1] + 0.2*(extent[1] - extent[0]);
y.domain(extent);
d3.select(".y").transition().duration(duration).call(yAxis);
};
var toggle = function(to, duration) {
if (to === "distribution") {
updateYAxis(dataContinuousGroups[0].data.concat(dataContinuousGroups[1].data).concat(dataContinuousGroups[2].data), 0);
updateXAxis("continuous", duration);
areas
.data(dataContinuousGroups)
.transition()
.duration(duration)
.attr("d", function(d) {
var gen = d3.line()
.x(function(p) {
return xContinuous(p.x);
})
.y(function(p) {
return y(p.y);
});
return gen(d.data);
});
thresholdLine
.style("opacity", 0);
g.select(".y.axis")
.style("opacity", 0);
g.select(".y-axis-label")
.style("opacity", 0);
} else {
y.domain([0, 1]);
d3.select(".y").transition().duration(duration).call(yAxis);
updateXAxis("discrete", duration);
areas
.data(dataDiscrete)
.transition()
.duration(duration)
.attr("d", function(d, i) {
let numPts = dataContinuousGroups[i].data.length - 2;
var path = d3.path()
path.moveTo(xDiscrete(d.x), y(0));
for (j=0; j<numPts; j++) {
path.lineTo(xDiscrete(d.x) + j*xDiscrete.bandwidth()/(numPts-1), y(d.y))
}
path.lineTo(xDiscrete(d.x) + xDiscrete.bandwidth(), y(0));
return path.toString();
});
thresholdLine
.transition()
.duration(0)
.delay(duration)
.style("opacity", 1)
.attr("y1", y(opts.threshold))
.attr("y2", y(opts.threshold));
g.select(".y.axis")
.transition()
.duration(0)
.delay(duration)
.style("opacity", 1);
g.select(".y-axis-label")
.transition()
.duration(0)
.delay(duration)
.style("opacity", 1);
}
};
// Add buttons
//container for all buttons
var allButtons = svg.append("g")
.attr("id", "allButtons");
//fontawesome button labels
var labels = ["B", "D"];
//colors for different button states
var defaultColor = "#E0E0E0";
var hoverColor = "#808080";
var pressedColor = "#000000";
//groups for each button (which will hold a rect and text)
var buttonGroups = allButtons.selectAll("g.button")
.data(labels)
.enter()
.append("g")
.attr("class", "button")
.style("cursor", "pointer")
.on("click", function(d, i) {
updateButtonColors(d3.select(this), d3.select(this.parentNode));
d3.select("#numberToggle").text(i + 1);
if (d === "D") {
toggle("distribution", transDuration);
} else {
toggle("discrete", transDuration);
}
})
.on("mouseover", function() {
if (d3.select(this).select("rect").attr("fill") != pressedColor) {
d3.select(this)
.select("rect")
.attr("fill", hoverColor);
}
})
.on("mouseout", function() {
if (d3.select(this).select("rect").attr("fill") != pressedColor) {
d3.select(this)
.select("rect")
.attr("fill", defaultColor);
}
});
var bWidth = 40; //button width
var bHeight = 25; //button height
var bSpace = 10; //space between buttons
var x0 = 20; //x offset
var y0 = 10; //y offset
//adding a rect to each toggle button group
//rx and ry give the rect rounded corner
buttonGroups.append("rect")
.attr("class", "buttonRect")
.attr("width", bWidth)
.attr("height", bHeight)
.attr("x", function(d, i) {
return x0 + (bWidth + bSpace) * i;
})
.attr("y", y0)
.attr("rx", 5) //rx and ry give the buttons rounded corners
.attr("ry", 5)
.attr("fill", defaultColor);
//adding text to each toggle button group, centered
//within the toggle button rect
buttonGroups.append("text")
.attr("class", "buttonText")
.attr("x", function(d, i) {
return x0 + (bWidth + bSpace) * i + bWidth / 2;
})
.attr("y", y0 + bHeight / 2)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "central")
.attr("fill", "white")
.text(function(d) {
return d;
});
function updateButtonColors(button, parent) {
parent.selectAll("rect")
.attr("fill", defaultColor);
button.select("rect")
.attr("fill", pressedColor);
}
toggle("distribution", 0);
setTimeout(() => {
toggle("discrete", transDuration);
}, 1000);
},
resize: function(width, height) {
// TODO: code to re-render the widget with a new size
}
};
}
});
Once I save the svg in the right folder, I'm also not sure how can I use it to replace the two buttons that I have.
It will probably be easiest and most self contained to grab the svg paths (and in this case a rect) and attach them to the svg with svg.append("defs") - no need to access any image file from the script. Inserting an svg straight from a file makes it trickier, for example, to color, .attr("fill",) won't work in this case.
Open the icon in a text editor, the data we want from the icon is:
<path d="M37.92,42.22c3.78-8,7-14.95,12.08-14.95h0c5,0,8.3,6.93,12.08,14.95,6.12,13,13.73,29.13,33.48,29.13h0v-2h0c-18.48,0-25.79-15.51-31.67-28C59.82,32.74,56.3,25.28,50,25.28h0c-6.3,0-9.82,7.46-13.89,16.09-5.88,12.47-13.19,28-31.67,28h0v2h0C24.18,71.35,31.8,55.2,37.92,42.22Z"/>
<rect y="72.72" width="100" height="2"/>
Then we can append them to the svg as defs, using a parent g, with:
var symbol = svg.append("defs")
.append("g")
.attr("id","bellcurve");
symbol.append("path")
.attr("d", "M37.92,42.22c3.78-8,7-14.95,12.08-14.95h0c5,0,8.3,6.93,12.08,14.95,6.12,13,13.73,29.13,33.48,29.13h0v-2h0c-18.48,0-25.79-15.51-31.67-28C59.82,32.74,56.3,25.28,50,25.28h0c-6.3,0-9.82,7.46-13.89,16.09-5.88,12.47-13.19,28-31.67,28h0v2h0C24.18,71.35,31.8,55.2,37.92,42.22Z" )
symbol.append("rect")
.attr("y", 72.72)
.attr("width",100)
.attr("height",2);
To use the icon, we only need to append it as a child of a g element (this allows us to scale it too, and since it's width is 100 pixels, this allows for easy scaling to any width:
svg.append("g")
.attr("transform","scale(0.4)")
.append("use")
.attr("xlink:href","#bellcurve")
Like any other svg element, we can set the stroke, fill, and stroke-width attributes. If setting the stroke-width to more than 2, you probably won't need to set the fill: the stroke will overlap it.
Here's a quick demonstration using your icon, scaling it and coloring it, and for fun, transitioning it:
var svg = d3.select("body").append("svg")
.attr("width", 400)
.attr("height", 400);
var symbol = svg.append("defs")
.append("g")
.attr("id","bellcurve");
symbol.append("path")
.attr("d", "M37.92,42.22c3.78-8,7-14.95,12.08-14.95h0c5,0,8.3,6.93,12.08,14.95,6.12,13,13.73,29.13,33.48,29.13h0v-2h0c-18.48,0-25.79-15.51-31.67-28C59.82,32.74,56.3,25.28,50,25.28h0c-6.3,0-9.82,7.46-13.89,16.09-5.88,12.47-13.19,28-31.67,28h0v2h0C24.18,71.35,31.8,55.2,37.92,42.22Z" )
symbol.append("rect")
.attr("y", 72.72)
.attr("width",100)
.attr("height",2);
svg.append("g")
.append("use")
.attr("xlink:href","#bellcurve")
.attr("fill","steelblue")
.attr("stroke","steelblue")
svg.append("g")
.attr("transform","translate(100,0)scale(0.5)")
.append("use")
.attr("xlink:href","#bellcurve")
.attr("fill","steelblue")
.attr("stroke","steelblue")
.attr("stroke-width",2)
svg.append("g")
.attr("transform","translate(100,50)scale(0.5)")
.append("use")
.attr("xlink:href","#bellcurve")
.attr("fill","steelblue")
.attr("stroke","steelblue")
.attr("stroke-width",5)
var transition = function() {
d3.select(this)
.transition()
.attr("stroke","orange")
.attr("fill","orange")
.duration(1000)
.transition()
.attr("stroke","steelblue")
.attr("fill","steelblue")
.duration(500)
.on("end",transition)
}
d3.selectAll("g").selectAll("use")
.each(transition);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
With that it should be fairly easy to append the image straight to a button. And when toggling which visualization is showing, you can toggle the button's fill.
Slick looking app by the way.
Related
I am creating a horizontal animated d3 chart. How do you reverse the x axis and position the bars in a more dynamic way.
Are the bars the correct width or is the xaxis scale correct? Using d3 version 4
//horizontal work in progress
http://jsfiddle.net/ueg3bjf7/
//vertical chart code this is based from
http://jsfiddle.net/myf1zhar/
$(document).ready(function() {
var $this = $(".barchart");
var w = $this.data("width");
var h = $this.data("height");
var data = $this.data("data");
var data = [{
"label": "Apples",
"value": 100
},
{
"label": "Pears",
"value": 120
},
{
"label": "Bananas",
"value": 20
}
];
var configurations = $this.data("configurations");
function colores_google(n) {
var colores_g = ["#f7b363", "#448875", "#2b2d39", "#c12f39", "#f8dd2f", "#1b91dc"];
return colores_g[n % colores_g.length];
}
//asess the margin bottom for the chart based on the max char label
var charLabelCount = [];
data.map(function(d) {
var labelStr = d.label.toString();
charLabelCount.push(labelStr.length);
})
var maxChars = charLabelCount.reduce(function(a, b) {
return Math.max(a, b);
});
var bottomMarg = 60;
if (maxChars > 15) {
bottomMarg = 170;
}
//bottom margin calculation
var margin = {
top: 15,
right: 20,
bottom: bottomMarg,
left: 40
},
width = w - margin.left - margin.right,
height = h - margin.top - margin.bottom;
var x = d3.scaleBand()
.range([0, width]);
var y = d3.scaleLinear()
.range([height, 0]);
var yAxis = d3.axisBottom(y);
var xAxis = d3.axisLeft(x);
var svg = d3.select($this[0])
.append("svg")
.attr("width", w)
.attr("height", h)
.attr("viewBox", "0 0 " + w + " " + h)
.attr("preserveAspectRatio", "xMidYMid meet")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr("class", "barchartg");
function sortBy(array, key) {
var sorted = array.sort(function(a, b) {
return parseFloat(b[key]) - parseFloat(a[key]);
});
return sorted;
}
var sortedMax = 45;
if (configurations) {
if (configurations[0]["maxValue"]) {
sortedMax = configurations[0]["maxValue"] + 5;
}
} else {
sortedMax = sortBy(data, "value")[0]["value"] + 5;
}
x.domain(data.map(function(d) {
return d.label;
}));
y.domain([0, sortedMax]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0,25)")
.call(xAxis);
svg.selectAll(".x.axis text")
.attr("transform", "rotate(-60) translate(-5,-5)")
.style("text-anchor", "end");
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("");
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("fill", function(d, i) {
return colores_google(i);
})
.attr("x", function(d) {
return 0;
})
.attr("width", function(d) {
return d.value;
})
.attr("y", function(d, i) {
return 45 + (i * 90);
})
.attr("height", function(d) {
return 50;
});
d3.selectAll("rect").transition()
.duration(500)
.delay(function(d, i) {
return 500 * i;
})
.attr("width", function(d) {
return 0;
})
setTimeout(function() {
d3.selectAll("rect").transition()
.duration(500)
.delay(function(d, i) {
return 600 * (3 - i);
})
.attr("width", function(d) {
return d.value;
})
}, 2000);
});
I will try to answer your questions.
How do you reverse the x axis
You have to change the domain of the axis
y.domain([sortedMax, 0]);
position the bars
You have to translate the axis to the width of your graph
svg.append("g").attr("transform", "translate(0, 300)").attr("class", "y axis")
Are the bars the correct width or is the xaxis scale correct?
You have to use a multiplier to calculate the width of each bar, using the max width of your graph and your max value. I have added the 25 pixels of the translate of the x axis
var mult = (w + 25) / sortedMax;
...
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("fill", function(d, i) {
return colores_google(i);
})
.attr("x", function(d) {
return 0;
})
.attr("width", function(d) {
return d.value * mult;
})
.attr("y", function(d, i) {
return 45 + (i * 90);
})
.attr("height", function(d) {
return 50;
});
...
setTimeout(function() {
d3.selectAll("rect").transition()
.duration(500)
.delay(function(d, i) {
return 600 * (3 - i);
})
.attr("width", function(d) {
return d.value * mult;
})
}, 2000);
You can see the result in this fiddle http://jsfiddle.net/jfLgawue/65/
I am trying to create a horizontal graph legend in D3.js. I am using a group element (g) as a container for all the legends and the individual legends (text) are also each wrapped inside a "g" element. The result is that the individual legends are stacked on top of each other rather than spaced out.
I have tried changing the x attribute on the legends and also transform/translate. Whilst the DOM shows that the x values are applied the legends don't move. So if the DOM shows the legend / g element is positioned at x = 200 it is still positioned at 0.
I have spent two days trying to solve this and probably looked at over 50 examples including anything I could find on StackExchange.
Below code is my latest attempt. It doesn't through any error and the x values are reflected in the DOM but the elements just won't move.
I have included the code covering the relevant bits (but not all code).
The legend container is added here:
/*<note>Add container to hold legends. */
var LegendCanvas = d3.select("svg")
.append("g")
.attr("class", "legend canvas")
.attr("x", 0)
.attr("y", 0)
.attr("width", Width)
.style("fill", "#ffcccc")
.attr("transform", "translate(0,15)");
There is then a loop through a json array of objects:
var PrevElemLength = 0;
/*<note>Loop through each data series, call the Valueline variable and plot the line. */
Data.forEach(function(Key, i) {
/*<note>Add the metric line(s). */
Svg.append("path")
.attr("class", "line")
.attr("data-legend",function() { return Key.OriginId })
/*<note>Iterates through the data series objects and applies a different color to each line. */
.style("stroke", function () {
return Key.color = Color(Key.UniqueName); })
.attr("d", Valueline(Key.DataValues));
/*<note>Add a g element to the legend container. */
var Legend = LegendCanvas.append("g")
.attr("class", "legend container")
.attr("transform", function (d, i) {
if (i === 0) {
return "translate(0,0)"
} else {
PrevElemLength += this.previousElementSibling.getBBox().width;
return "translate(" + (PrevElemLength) + ",0)"
}
});
/*<note>Adds a rectangle to pre-fix each legend. */
Legend.append("rect")
.attr("width", 5)
.attr("height", 5)
.style("fill", function () {
return Key.color = Color(Key.UniqueName); });
/*<note>Adds the legend text. */
Legend.append("text")
.attr("x", function() {
return this.parentNode.getBBox().width + 5;
})
/*.attr("y", NetHeight + (Margin.bottom/2)+ 10) */
.attr("class", "legend text")
.style("fill", function () {
return Key.color = Color(Key.UniqueName); })
.text(Key.UniqueName);
Here is a screen shot of what the output looks like:
enter image description here
Any help on how to create a horizontal legend (without over lapping legends) would be much appreciated. Chris
The problem is you are using local variables d and i as function parameters while setting the transform attribute. Parameter i in local scope overrides the actual variable. The value of local variable i would be always zero as there is no data bind to that element.
var Legend = LegendCanvas.append("g")
.attr("class", "legend container")
.attr("transform", function (d, i) { //Remove i
if (i === 0) {
return "translate(0,0)"
} else {
PrevElemLength += this.previousElementSibling.getBBox().width;
return "translate(" + (PrevElemLength) + ",0)"
}
});
I have also made slight updates to the code for improvements.
var LegendCanvas = d3.select("svg")
.append("g")
.attr("class", "legend canvas")
.attr("x", 0)
.attr("y", 0)
.attr("width", 500)
.style("fill", "#ffcccc")
.attr("transform", "translate(0,15)");
var PrevElemLength = 0;
var Data = [{
OriginId: 1,
UniqueName: "Some Long Text 1"
}, {
OriginId: 2,
UniqueName: "Some Long Text 2"
}];
/*<note>Loop through each data series, call the Valueline variable and plot the line. */
var Color = d3.scale.category10();
Data.forEach(function(Key, i) {
/*<note>Add a g element to the legend container. */
var Legend = LegendCanvas.append("g")
.attr("class", "legend container")
.attr("transform", function() {
if (i === 0) {
return "translate(0,0)"
} else {
var marginLeft = 5;
PrevElemLength += this.previousElementSibling.getBBox().width;
return "translate(" + (PrevElemLength + marginLeft) + ",0)"
}
});
/*<note>Adds a rectangle to pre-fix each legend. */
Legend.append("rect")
.attr("width", 5)
.attr("height", 5)
.style("fill", function() {
return Key.color = Color(Key.UniqueName);
});
/*<note>Adds the legend text. */
Legend.append("text")
.attr("x", function() {
return this.parentNode.getBBox().width + 5;
})
.attr("dy", "0.4em")
.attr("class", "legend text")
.style("fill", function() {
return Key.color = Color(Key.UniqueName);
})
.text(Key.UniqueName);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg height=500 width=500></svg>
The d3 way of implementation(Using data binding) would be as follows
var LegendCanvas = d3.select("svg")
.append("g")
.attr("class", "legend canvas")
.attr("x", 0)
.attr("y", 0)
.attr("width", 500)
.style("fill", "#ffcccc")
.attr("transform", "translate(0,15)");
var Data = [{
OriginId: 1,
UniqueName: "Some Long Text 1"
}, {
OriginId: 2,
UniqueName: "Some Long Text 2"
}];
var Color = d3.scale.category10();
var Legend = LegendCanvas.selectAll(".legend")
.data(Data)
.enter()
.append("g")
.attr("class", "legend container");
Legend.append("rect")
.attr("width", 5)
.attr("height", 5)
.style("fill", function(Key) {
return Key.color = Color(Key.UniqueName);
});
Legend.append("text")
.attr("x", function() {
return this.parentNode.getBBox().width + 5;
})
.attr("dy", "0.4em")
.attr("class", "legend text")
.style("fill", function(Key) {
return Key.color = Color(Key.UniqueName);
})
.text(function(Key){ return Key.UniqueName });
var PrevElemLength = 0;
Legend.attr("transform", function(d, i) {
if (i === 0) {
return "translate(0,0)"
} else {
var marginLeft = 5;
PrevElemLength += this.previousElementSibling.getBBox().width;
return "translate(" + (PrevElemLength + marginLeft) + ",0)"
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg width=500 height=500></svg>
Try this :
//Legend
var legend = vis.selectAll(".legend")
.data(color.domain())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("image")
.attr("x", 890)
.attr("y", 70)
.attr("width", 20)
.attr("height", 18)
.attr("xlink:href",function (d) {
return "../assets/images/dev/"+d+".png";
})
legend.append("text")
.attr("x", 910)
.attr("y", 78)
.attr("dy", ".35em")
.style("text-anchor", "start")
.text(function(d) {
return d;
});
I've converted Mike Botock's Hierarchical Bar Chart to v4 and made some tweaks to fit my needs(vertical, tool-tip, widths of bars fit canvas, etc).
Now I'm trying to make this a stacked bar chart. In my JSON file I have two types of downtime Machine and Die. For the original chart I just add these up to get my overall but now i want to stack them and I'm unsure how to pull these out separately after doing a root.sum on the Hierarchy. This is my first chart so pardon some of the coding but feel free to correct me on anything. I also could clean some things up with if statements but I'm leaving everything separate as it is easier to troubleshoot. Any thoughts on how to make stacked hierarchical bar chart would be appreciated. Even if it means throwing away this code and starting over.
<body>
<script src="d3.min.js"></script>
<script>
//canvas variables
var margin = { top: 30, right: 120, bottom: 300, left: 120 },
width = 960 - margin.left - margin.right,
height = 800 - margin.top - margin.bottom;
// scale y on canvas from largest number to 0
var y = d3.scaleLinear().range([height, 0]);
var barWidth;
var barPadding = 5;
var oldBarWidth = width;
var depth = 0;
var color = d3.scaleOrdinal()
.range(["steelblue", "#ccc"]);
var duration = 750,
delay = 25;
//attach SVG to body with canvas variables declared above
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var divTooltip = d3.select("body").append("div").attr("class", "toolTip");
//attach a rectangle to the entire background for drilling
svg.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height)
.on("click", drillUp);
//append axis to the SVG
svg.append("g")
.attr("class", "yaxis");
svg.append("g")
.append("line")
.attr("transform", "translate(0," + height + ")")
.attr("x1", width)
.attr("stroke", "black")
.attr("shape-rendering", "crispEdges");
//import JSON file
d3.json("data/drilljson2.json", function (error, root) {
if (error) throw error;
//declare root of the JSON file
root = d3.hierarchy(root);
//add all children in hierarchy and get value for all parents
root.sum(function (d) { return (+d.DieDowntime + (+d.MachineDowntime)); });
//scale the 'Y' domain/axis from 0 to root value
y.domain([0, root.value]).nice();
//call the drill down function
drillDown(root, 0);
drillDown(root.children[3], 3);
});
function drillDown(d, i) {
if (!d.children) return;
// get the number of children to parent and calculate barWidth and keep track of depth of drill down.
numChildNodes = d.children.length;
barWidth = (width / numChildNodes) - barPadding;
depth += 1;
var end = duration + numChildNodes * delay;
// Mark any currently-displayed bars as exiting.
var exit = svg.selectAll(".enter")
.attr("class", "exit");
// Entering nodes immediately obscure the clicked-on bar, so hide it.
exit.selectAll("rect").filter(function (p) { return p === d; })
.style("fill-opacity", 0);
// Enter the new bars for the clicked-on data.
// Entering bars are immediately visible.
var enter = drillDownBars(d)
.attr("transform", stackDown(i))
.attr("width", oldBarWidth)
.style("opacity", 1);
// Update the y-scale domain.
y.domain([0, d3.max(d.children, function (d) { return d.value; })]).nice();
// Have the text fade-in, even though the bars are visible.
// Color the bars as parents; they will fade to children if appropriate.
enter.select("text").style("fill-opacity", 0);
enter.select("rect").style("fill", color(true));
// Update the y-axis.
svg.selectAll(".yaxis").transition()
.duration(duration)
.call(d3.axisLeft(y));
// Transition entering bars to their new position.
var enterTransition = enter.transition()
.duration(duration)
.delay(function (d, i) { return i * delay; })
.style("opacity", 1)
.attr("transform", function (d, i) { var transBar = (barWidth +barPadding) * i +barPadding; return "translate("+ transBar + ")"; });
// Transition entering text.
enterTransition.select("text")
.attr("transform", function (d) { return "translate("+(barWidth/2)+","+((height+5) + 10 * depth)+")rotate(90)" })
// working .attr("y", height + 15)
.style("fill-opacity", 1);
// Transition entering rects to the new y-scale.
enterTransition.select("rect")
.attr("y", function (d) { return y(d.value); })
.attr("height", function (d) { return height - y(d.value); })
.attr("width",barWidth)
.style("fill", function (d) { return color(!!d.children); });
// Transition exiting bars to fade out.
var exitTransition = exit.transition()
.duration(duration)
.style("opacity", 0)
.remove();
// Transition exiting bars to the new y-scale.
exitTransition.selectAll("rect")
.attr("y", function (d) { return y(d.value); })
.attr("height", function (d) { return height - y(d.value); });
// Rebind the current node to the background.
svg.select(".background")
.datum(d)
.transition()
.duration(end);
d.index = i;
oldBarWidth = barWidth;
}
function drillUp(d) {
if (!d.parent || this.__transition__) return;
numChildNodes = d.parent.children.length;
barWidth = (width / numChildNodes) - barPadding;
depth -= 1;
var end = duration + d.children.length * delay;
// Mark any currently-displayed bars as exiting.
var exit = svg.selectAll(".enter")
.attr("class", "exit");
// Enter the new bars for the clicked-on data's parent.
var enter = drillUpBars(d.parent)
.attr("transform", function (d, i) {
transBarWidth = (barWidth + barPadding) * i + barPadding;
return "translate(" + transBarWidth + "," + 0 + ")";
})
.style("opacity", 0);
// Color the bars as appropriate.
// Exiting nodes will obscure the parent bar, so hide it.
enter.select("rect")
.style("fill", function (d) { return color(!!d.children); })
.filter(function (p) { return p === d; })
.style("fill-opacity", 0);
// Update the y-scale domain.
y.domain([0, d3.max(d.parent.children, function (d) { return d.value; })]).nice();
// Update the y-axis.
svg.selectAll(".yaxis").transition()
.duration(duration)
.call(d3.axisLeft(y));
// Transition entering bars to fade in over the full duration.
var enterTransition = enter.transition()
.duration(end)
.style("opacity", 1);
// Transition entering rects to the new y-scale.
// When the entering parent rect is done, make it visible!
enterTransition.select("rect")
.attr("y", function (d) { return y(d.value); })
.attr("height", function (d) { return height - y(d.value); })
.on("end", function (p) { if (p === d) d3.select(this).style("fill-opacity", null); });
// Transition entering text.
enterTransition.select("text")
.attr("transform", function (d) { return "translate("+(barWidth/2)+","+((height+5) + 10 * depth)+")rotate(90)" })
.style("fill-opacity", 1);
// Transition exiting bars to the parent's position.
var exitTransition = exit.selectAll("g").transition()
.duration(duration)
.delay(function (d, i) { return i * delay; })
.attr("transform", stackUp(d.index));
// Transition exiting text to fade out.
exitTransition.select("text")
.style("fill-opacity", 0);
// Transition exiting rects to the new scale and fade to parent color.
exitTransition.select("rect")
.attr("y", function (d) { return y(d.value); })
.attr("height", function (d) { return height - y(d.value); })
.attr("width", barWidth)
.style("fill", color(true));
// Remove exiting nodes when the last child has finished transitioning.
exit.transition()
.duration(end)
.remove();
// Rebind the current parent to the background.
svg.select(".background")
.datum(d.parent)
.transition()
.duration(end);
oldBarWidth = barWidth;
}
// Creates a set of bars for the given data node, at the specified index.
function drillUpBars(d) {
var bar = svg.insert("g")
.attr("class", "enter")
.selectAll("g")
.data(d.children)
.enter().append("g")
.style("cursor", function (d) { return !d.children ? null : "pointer"; })
.on("click", drillDown);
bar.append("text")
.attr("dx", ".35em")
.attr("text-anchor", "middle")
.attr("transform", function (d) { return "translate(" + barWidth / 2 + "," + (height + 15) + ") rotate(90)" })
.text(function (d) { return d.data.name; });
bar.append("rect")
.attr("y", function (d) { return y(d.value); } )
.attr("height", function (d) { return height - y(d.value); })
.attr("width", barWidth)
.attr("stroke-width", 1)
.attr("stroke", "white");
return bar;
}
function drillDownBars(d) {
var bar = svg.insert("g")
.attr("class", "enter")
.selectAll("g")
.data(d.children)
.enter().append("g")
.style("cursor", function (d) { return !d.children ? null : "pointer"; })
.on("click", drillDown)
.on("mouseover", mouseover)
.on("mousemove", function (d) {
divTooltip
.text(d.data.name +" " + d.value)
.style("left", (d3.event.pageX - 34) + "px")
.style("top", (d3.event.pageY - 30) + "px");
});
bar.append("text")
.attr("dx", ".35em")
.attr("text-anchor", "middle")
.attr("transform", function (d) { return "translate(" + barWidth / 2 + "," + (height + 15 - y(d.value)) + ") rotate(90)" })
.text(function (d) { return d.data.name; });
bar.append("rect")
.attr("height", function (d) { return height - y(d.value); })
.attr("width", oldBarWidth)
.attr("stroke-width", 1)
.attr("stroke", "white");
return bar;
}
//Creates a stack of bars
function stackDown(i) {
var x0 = (oldBarWidth + barPadding) * i + barPadding;
var y0 = height;
return function (d) {
y0 -= height - y(d.value);
var ty = "translate(" + x0 + "," + y0 + ")";
return ty;
};
}
//
function stackUp(i) {
var x0 = barWidth * i + (barPadding * (i + 1));
var y0 = 0;
return function (d) {
var ty = "translate(" + x0 + "," + y0 + ")";
y0 -= height - y(d.value);
return ty;
};
}
function mouseover() {
divTooltip.style("display", "inline");
}
</script>
Here is a piece of the JSON file which could also use a little cleaning but minor details for now.
{
"name" : "Down Time",
"children" : [{
"name" : "2013",
"children" : [{
"name" : "May 2013",
"children" : [{
"name" : "21 May 2013",
"children" : [{
"name" : "110",
"children" : [{
"MachineDowntime" : ".00000000000000000000",
"DieDowntime" : ".50000000000000000000"
}
]
}, {
"name" : "115",
"children" : [{
"MachineDowntime" : "5.23333333333333333300",
"DieDowntime" : ".00000000000000000000"
}
]
}
]
}, {
"name" : "22 May 2013",
"children" : [{
"name" : "115",
"children" : [{
"MachineDowntime" : "2.96666666666666666730",
"DieDowntime" : ".00000000000000000000"
}
]
}, {
"name" : "110",
"children" : [{
"MachineDowntime" : ".00000000000000000000",
"DieDowntime" : "10.36666666666666667000"
I am trying to make my tooltip read my data. But it won't. How do i make it read the data?
I do not understand why I can apply text labels in my chart by writing
.text(function(d) { return d; });
while the tooltip won't read it.
var data = {
labels: [
'Trøndelag', 'Innlandet', 'Oslo','Nordland','Sør-Øst', 'Alle distr.',
'Øst', 'Sør-Vest', 'Møre og R.',
'Troms', 'Vest', 'Finnmark',
],
series: [
{
label: 'Svært stor tillit',
values: [32, 29, 29, 22, 27, 27,31,25,24,26,26,20,24]
},
{
label: 'Ganske stor tillit',
values: [55,54,53,58,53,53,49,53,54,51,48,53,48]
},
{
label: 'Verken stor eller liten tillit',
values: [7,12,13,14,14,16,14,15,16,19,19,15]
},
{
label: 'Ganske liten tillit',
values: [4,4,3,2,3,3,3,3,4,5,4,4,7]
},
{
label: 'Svært liten tillit',
values: [1,1,2,3,3,2,1,3,3,1,2,4,6]
},
{
label: 'Vet ikke',
values: [0,0,1,0,0,0,1,1,0,0,0,0,1]
},
{
label: 'Ubesvart',
values: [0,0,0,0,0,0,0,0,0,0,0,0,0]
}
]
};
var margin = {top: 20, right: 5, bottom: 20, left: 5},
width = parseInt(d3.select('.chart').style('width'), 10),
width = width - margin.left - margin.right,
chartHeight = 1310,
groupHeight = barHeight * data.series.length,
gapBetweenGroups = 0,
spaceForLabels =62,
spaceForLegend = 64,
barHeight=14;
var zippedData = [];
for (var i=0; i<data.labels.length; i++) {
for (var j=0; j<data.series.length; j++) {
zippedData.push(data.series[j].values[i]);
}
}
// Color scale
var color = d3.scale.category20c();
var x = d3.scale.linear()
.domain([0, d3.max(zippedData)])
.range([0, width]);
var y = d3.scale.linear()
.range([chartHeight + gapBetweenGroups, 0]);
d3.select(window).on('resize', resize);
function resize (){
width = parseInt(d3.select('.chart').style('width'),10);
width= width - margin.left - margin.right;
x.range([0,width]);
}
var yAxis = d3.svg.axis()
.scale(y)
.tickFormat('')
.tickSize(0)
.orient("left");
// Specify the chart area and dimensions
var chart = d3.select(".chart")
.attr("width", spaceForLabels + width + spaceForLegend)
.attr("height", chartHeight);
// Create bars
var bar = chart.selectAll("g")
.data(zippedData)
.enter().append("g")
.attr("transform", function(d, i) {
return "translate(" + spaceForLabels + "," + (i * barHeight + gapBetweenGroups * (0.5 + Math.floor(i/data.series.length))) + ")";
})
;
var legendPlass = 150;
var tooltip = d3.select("body")
.append("div")
.attr("class", "d3-tip")
.style("position", "absolute")
.style("opacity", 0);
// Create rectangles of the correct width
bar.append("rect")
.attr("fill", function(d,i) { return color(i % data.series.length); })
.attr("class", "bar")
.attr("width", x)
.attr('y', legendPlass )
.attr("height", barHeight - 1)
;
// Add text label in bar
bar.append("text")
.attr("x", function(d) { return x(d) - 3; })
.attr("y", legendPlass + barHeight / 2)
.attr("fill", "red")
.attr("dy", ".35em")
.text(function(d) { return d; });
// Draw labels
bar.append("text")
.attr("class", "label")
.attr("x", function(d) { return - 5; })
.attr("y", legendPlass)
.attr("dy", "1em")
.text(function(d,i) {
if (i % data.series.length === 0)
return data.labels[Math.floor(i/data.series.length)];
else
return ""});
chart.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + spaceForLabels + ", " + -gapBetweenGroups/2 + ")")
.call(yAxis);
//CREATING THE TOOLTIP
chart.selectAll(".bar")
.on("click", function() {
tooltip.style("opacity", 0); })
.on("click", function(d) {
var pos = d3.mouse(this);
tooltip
.transition()
.duration(500)
.style("opacity", 1)
.style("left", d3.event.x + "px")
.style("top", d3.event.y + "px")
.text(function(d) { return d; });
});
// Draw legend
var legendRectSize = 16,
legendSpacing = 4;
var legend = chart.selectAll('.legend')
.data(data.series)
.enter()
.append('g')
.attr('transform', function (d, i) {
var height = legendRectSize + legendSpacing;
var offset = -gapBetweenGroups/2;
var horz = spaceForLegend;
var vert = i * height - offset;
return 'translate(' + horz + ',' + vert + ')';
});
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', function (d, i) { return color(i); })
.style('stroke', function (d, i) { return color(i); });
legend.append('text')
.attr('class', 'legend')
.attr('x', legendRectSize + legendSpacing )
.attr('y', legendRectSize - legendSpacing)
.text(function (d) { return d.label; });
You need to append data to it to be able to read. You have this :
var tooltip = d3.select("body")
.append("div")
.attr("class", "d3-tip")
.style("position", "absolute")
.style("opacity", 0);
Needs to be like this :
var tooltip = d3.select("body")
.append("div")
.attr("class", "d3-tip")
.style("position", "absolute")
.style("opacity", 0);
var tooltipWithData = tooltip.data(data).enter();
Then use this later :
tooltipWithData
.transition()
.duration(500)
.style("opacity", 1)
.style("left", d3.event.x + "px")
.style("top", d3.event.y + "px")
.text(function(d) { return d; });
I need help adding a second series of data to my bar chart. Currently I'm populating the bars from the glob key in my dataset. This will be the first series. The second series I would like to be is local.
How do I go about adding that?
Play with my JSFiddle here.
var w = 300;
var h = 200;
var colors = ["#377EB8", "#4DAF4A"];
var dataset = [
{"keyword": "payday loans", "glob": 1500000, "local": 673000, "cpc": "14.11"},
{"keyword": "title loans", "glob": 165000, "local": 165000, "cpc": "12.53" },
{"keyword": "personal loans", "glob": 550000, "local": 301000, "cpc": "6.14"}
];
var data = [[1500000, 165000, 550000],
[673000, 165000, 301000]];
var tdata = d3.transpose(dataset.glob, dataset.local);
var series = 2; // Global & Local
var x0Scale = d3.scale.ordinal()
.domain(d3.range(dataset.length))
.rangeRoundBands([0, w], 0.05);
var x1Scale = d3.scale.ordinal()
.domain(d3.range(dataset.length))
.rangeRoundBands([0, w], 0.05);
var yScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(d) {return d.glob;})])
.range([0, h]);
var glob = function(d) {
return d.glob;
};
var cpc = function(d) {
return d.cpc;
};
var commaFormat = d3.format(',');
//SVG element
var svg = d3.select("#searchVolume")
.append("svg")
.attr("width", w)
.attr("height", h);
// Graph Bars
svg.selectAll("rect")
.data(dataset, cpc, glob)
.enter()
.append("rect")
.attr("x", function(d, i){
return x0Scale(i);
})
.attr("y", function(d) {
return h - yScale(d.glob);
})
.attr("width", x0Scale.rangeBand())
.attr("height", function(d) {
return yScale(d.glob);
})
.attr("fill", colors[1])
.on("mouseover", function(d) {
//Get this bar's x/y values, then augment for the tooltip
var xPosition = parseFloat(d3.select(this).attr("x")) + x0Scale.rangeBand() / 3;
var yPosition = parseFloat(d3.select(this).attr("y")) + yScale;
//Update Tooltip Position & value
d3.select("#tooltip")
.style("left", xPosition + "px")
.style("top", yPosition + "px")
.select("#cpcVal")
.text(d.cpc);
d3.select("#tooltip").classed("hidden", false);
})
.on("mouseout", function() {
//Remove the tooltip
d3.select("#tooltip").classed("hidden", true);
});
//Create labels
svg.selectAll("text")
.data(dataset, glob)
.enter()
.append("text")
.text(function(d) {
return commaFormat(d.glob);
})
.attr("text-anchor", "middle")
.attr("x", function(d, i) {
return x0Scale(i) + x0Scale.rangeBand() / 2;
})
.attr("y", function(d) {
return h - yScale(d.glob) + 14;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "white");
It's easiest to create an svg group (<g>) element for each set of data, and then add the individual parts to each group. For example, roughly:
http://jsfiddle.net/WXMwv/1/
var sets = svg.selectAll(".set")
.data(dataset)
.enter().append("g")
.attr("class","set")
.attr("transform",function(d,i){
return "translate(" + x0Scale(i) + ",0)"
})
.on("mouseover", function(d,i) {
//Create x value from data index
var xPosition = parseFloat(x0Scale(i) + x0Scale.rangeBand() / 6);
var yPosition = 0;
//Update Tooltip Position & value
d3.select("#tooltip")
.style("left", xPosition + "px")
.style("top", yPosition + "px")
.select("#cpcVal")
.text(d.cpc);
d3.select("#tooltip").classed("hidden", false);
})
.on("mouseout", function() {
//Remove the tooltip
d3.select("#tooltip").classed("hidden", true);
});
sets.append("rect")
.attr("class","glob")
.attr("width", x0Scale.rangeBand()/2)
.attr("y", function(d) {
return yScale(d.glob);
})
.attr("height", function(d){
return h - yScale(d.glob);
})
.attr("fill", colors[1])
sets.append("rect")
.attr("class","local")
.attr("width", x0Scale.rangeBand()/2)
.attr("y", function(d) {
return yScale(d.local);
})
.attr("x", x0Scale.rangeBand()/2)
.attr("height", function(d){
return h - yScale(d.local);
})
.attr("fill", colors[0])
The text elements are left as an exercise for the reader :)