I've made a scatterplot and a choropleth map in the same web page. Data is stored in a .CSV and .json, and elements are linked with a "name" field.
I've made a tooltip on mouseover on both. I want now some interactivity beetween them: when mouse is over an element on scatterplot, this element on choropleth react and when mouse is over choropleth map scatterplot react.
Scatterplot and choropleth are in differents div with specifics ID and I don't how can I refeer from one to an other. I tried d3.select("this#scatterplot"); like this example but it doesn't work for me.
How can I select elements in differents DIV and differents functions ?
I want something like this :
function handleMouseOverMap(d, i) {
d3.select('this#choropleth').style('stroke-width', 3);
d3.select('this#scatterplot').attr('r', 8);
}
function handleMouseOverGraph(d, i) {
d3.select('this#scatterplot').attr('r', 8);
d3.select('this#choropleth').style('stroke-width', 3);
}
Code
<div id="scatterplot"></div>
<div id="choropleth"></div>
<script>
d3.queue()
.defer(d3.csv, 'data.csv', function (d) {
return {
name: d.name,
sau: +d.sau,
uta: +d.uta
}
})
.defer(d3.json, 'dept.json')
.awaitAll(initialize)
var color = d3.scaleThreshold()
.domain([150000, 300000, 450000])
.range(['#5cc567', '#e7dc2b', '#e59231', '#cb0000'])
function initialize(error, results) {
if (error) { throw error }
var data = results[0]
var features = results[1].features
var components = [
choropleth(features),
scatterplot(onBrush)
]
function update() {
components.forEach(function (component) { component(data) })
}
function onBrush(x0, x1, y0, y1) {
var clear = x0 === x1 || y0 === y1
data.forEach(function (d) {
d.filtered = clear ? false
: d.uta < x0 || d.uta > x1 || d.sau < y0 || d.sau > y1
})
update()
}
update()
}
/* Graphique */
function scatterplot(onBrush) {
var margin = { top: 10, right: 15, bottom: 40, left: 75 }
var width = 680 - margin.left - margin.right
var height = 550 - margin.top - margin.bottom
var x = d3.scaleLinear()
.range([0, width])
var y = d3.scaleLinear()
.range([height, 0])
// Tooltip
var xValue = function(d) { return d.sau;};
var yValue = function(d) { return d.uta;};
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var xAxis = d3.axisBottom()
.scale(x)
.tickFormat(d3.format(''))
var yAxis = d3.axisLeft()
.scale(y)
.tickFormat(d3.format(''))
// Selection
var brush = d3.brush()
.extent([[0, 0], [width, height]])
.on('start brush', function () {
var selection = d3.event.selection
var x0 = x.invert(selection[0][0])
var x1 = x.invert(selection[1][0])
var y0 = y.invert(selection[1][1])
var y1 = y.invert(selection[0][1])
onBrush(x0, x1, y0, y1)
})
var svg = d3.select('#scatterplot')
.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 bg = svg.append('g')
var gx = svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
var gy = svg.append('g')
.attr('class', 'y axis')
gx.append('text')
.attr('x', width)
.attr('y', 35)
.style('text-anchor', 'end')
.style('fill', '#000')
.style('font-weight', 'bold')
.text('UTA')
gy.append('text')
.attr('transform', 'rotate(-90)')
.attr('x', 0)
.attr('y', -55)
.style('text-anchor', 'end')
.style('fill', '#000')
.style('font-weight', 'bold')
.text('SAU - ha')
svg.append('g')
.attr('class', 'brush')
.call(brush)
return function update(data) {
x.domain(d3.extent(data, function (d) { return d.uta })).nice()
y.domain(d3.extent(data, function (d) { return d.sau })).nice()
gx.call(xAxis)
gy.call(yAxis)
var bgRect = bg.selectAll('rect')
.data(d3.pairs(d3.merge([[y.domain()[0]], color.domain(), [y.domain()[1]]])))
bgRect.exit().remove()
bgRect.enter().append('rect')
.attr('x', 0)
.attr('width', width)
.merge(bgRect)
.attr('y', function (d) { return y(d[1]) })
.attr('height', function (d) { return y(d[0]) - y(d[1]) })
.style('fill', function (d) { return color(d[0]) })
var circle = svg.selectAll('circle')
.data(data, function (d) { return d.name })
circle.exit().remove()
circle.enter().append('circle')
.attr('r', 4)
.style('stroke', '#fff')
.merge(circle)
.attr('cx', function (d) { return x(d.uta) })
.attr('cy', function (d) { return y(d.sau) })
.style('fill', function (d) { return color(d.sau) })
.style('opacity', function (d) { return d.filtered ? 0.5 : 1 })
.style('stroke-width', function (d) { return d.filtered ? 1 : 2 })
// Event
.on("mouseover", function(d) {
tooltipOverGraph.call(this, d);
handleMouseOverGraph.call(this, d);
})
.on("mouseout", function(d) {
tooltipOutGraph.call(this, d);
handleMouseOutGraph.call(this, d);
})
}
// Tooltip
function tooltipOverGraph(d) {
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html(d["name"] + "<br>" + xValue(d)
+ " ha" +", " + yValue(d) + " UTA" )
.style("left", (d3.event.pageX + 5) + "px")
.style("top", (d3.event.pageY - 28) + "px");
}
function tooltipOutGraph(d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
}
}
// Create Event Handlers for mouse
function handleMouseOverGraph(d, i) {
d3.select(this).attr('r', 8);
}
function handleMouseOutGraph(d, i) {
d3.select(this).attr('r', 4);
}
/* Carte */
function choropleth(features) {
var width = 680
var height = 550
// Tooltip
var xValue = function(d) { return d.sau;};
var yValue = function(d) { return d.uta;};
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// Projection et centrage de la carte
var projection = d3.geoMercator()
.center([ 3, 46.5 ])
.scale([width * 3.1])
.translate([width / 2, height / 2])
var path = d3.geoPath().projection(projection)
var svg = d3.select('#choropleth')
.append('svg')
.attr('width', width)
.attr('height', height)
svg.selectAll('path')
.data(features)
.enter()
.append('path')
.attr('d', path)
.style('stroke', '#fff')
.style('stroke-width', 1)
// Event
.on("mouseover", function(d) {
tooltipOverMap.call(this, d);
handleMouseOverMap.call(this, d);
})
.on("mouseout", function(d) {
tooltipOutMap.call(this, d);
handleMouseOutMap.call(this, d);
})
// Tooltip
function tooltipOverMap(d) {
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html(d["name"] + "<br>" + xValue(d)
+ " ha" +", " + yValue(d) + " UTA" )
.style("left", (d3.event.pageX + 5) + "px")
.style("top", (d3.event.pageY - 28) + "px");
}
function tooltipOutMap(d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
}
return function update(data) {
svg.selectAll('path')
.data(data, function (d) { return d.name || d.properties.name })
.style('fill', function (d) { return d.filtered ? '#ddd' : color(d.sau) })
}
}
// Create Event Handlers for mouse
function handleMouseOverMap(d, i) {
d3.select(this).style('stroke-width', 3);
}
function handleMouseOutMap(d, i) {
d3.select(this).style('stroke-width', 1);
}
</script>
Example
First, add distinct class attributes to the rect and circle items when you enter + append them:
bgRect.enter().append('rect')
.attr('class', function(d,i) { return 'classRect' + i; })
circle.enter().append('circle')
.attr('class', function(d,i) { return 'classCircle' + i; })
Then, update your mouse over functions:
function handleMouseOverMap(d) {
// update the choropleth //
d3.select(d).style('stroke-width', 3);
// update the scatterplot //
// capture the number contained in class (e.g. "1" for "classRect1")
var i = d3.select(d).class.substr(-1);
// select corresponding circle in scatter and update
d3.select('circle.classCircle'+i).attr('r', 8);
}
function handleMouseOverGraph(d) {
// update the scatter //
d3.select(d).attr('r', 8);
// update the choropleth //
// capture the number contained in class (e.g. "1" for "classCircle1")
var i = d3.select(d).class.substr(-1);
// select corresponding rect in the choropleth and update
d3.select('rect.classRect'+i).style('stroke-width', 3);
}
Related
I have a data legend SVG and I want to set the height and width of this SVG to be equal to the number of g which is based on the data. But how do I achieve this? My SVG height and width is always not according to the my G. I tried node().getBBox() but it is still not giving me the right height and width.
Here is my code :
var legend = d3.select(".svgLegend")
.append('svg')
.attr("id", "legend")
.append('g')
.attr("class", "mainGroup")
.attr('legend', true)
var itemEnter = legend.selectAll('g.legendItem')
.data(legendData)
.enter()
.append('g')
.attr('class', function (d) {
return 'legendItem ' + safe_name(d.name);
})
itemEnter.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', '10')
.attr('height', '10')
.style('fill', function (d) {
return color(d.name);
})
.attr('transform', 'translate(10,6)')
.attr('class', function (d) {
return 'legendRect ' + safe_name(d.name);
})
itemEnter.append('text')
.attr('x', 0)
.attr('y', 0)
.attr('class', 'legendText')
.text(function (d) { return d.name })
.attr('transform', 'translate(25, 15)')
itemEnter.selectAll("text").each(function () {
var textLength = this.getComputedTextLength();
itemEnter.attr("transform", function (d, i) { return "translate(" + i % 8 * (textLength + 60) + "," + Math.floor(i / 8) * itemHeight + ")"; })
})
Legend Data :
[
{
"name":"Malaysia",
"value":350,
"percentage":"48.61"
},
{
"name":"England",
"value":300,
"percentage":"41.67"
},
{
"name":"China",
"value":400,
"percentage":"55.56"
},
{
"name":"South Korea",
"value":600,
"percentage":"83.33"
}
]
What I want to achieve is that the svg's height and width is exact same as itemEnter's height and width.
You can use the values from getClientBoundingRect() to set the width and height of your SVG:
var bRect = legend.node().getBoundingClientRect()
svg.attr('width', bRect.width + 10)
.attr('height', bRect.height)
(adding in an extra 10px to the width for safety)
Demo:
var legendData = [
{
"name":"Malaysia",
"value":350,
"percentage":"48.61"
},
{
"name":"England",
"value":300,
"percentage":"41.67"
},
{
"name":"China",
"value":400,
"percentage":"55.56"
},
{
"name":"South Korea",
"value":600,
"percentage":"83.33"
}
]
function safe_name (t) {
return t.replace(/\W/g, '_')
}
function color (d) {
var colors = {
China: 'deepskyblue',
'South Korea': 'deeppink',
England: 'red',
Malaysia: 'goldenrod'
}
return colors[d]
}
var svg = d3.select(".svgLegend")
.append('svg')
.attr("id", "legend")
var legend = svg
.append('g')
.attr("class", "mainGroup")
.attr('legend', true)
var itemEnter = legend.selectAll('g.legendItem')
.data(legendData)
.enter()
.append('g')
.attr('class', function (d) {
return 'legendItem ' + safe_name(d.name);
})
itemEnter.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', '10')
.attr('height', '10')
.style('fill', function (d) {
return color(d.name);
})
.attr('transform', 'translate(10,6)')
.attr('class', function (d) {
return 'legendRect ' + safe_name(d.name);
})
itemEnter.append('text')
.attr('x', 0)
.attr('y', 0)
.attr('class', 'legendText')
.text(function (d) { return d.name })
.attr('transform', 'translate(25, 15)')
var itemHeight = 25
itemEnter.selectAll("text")
.each(function () {
var textLength = this.getComputedTextLength();
itemEnter.attr("transform", function (d, i) { return "translate(" + i % 8 * (textLength + 60) + "," + Math.floor(i / 8) * itemHeight + ")"; })
})
var bRect = legend.node().getBoundingClientRect()
svg.attr('width', bRect.width + 10)
.attr('height', bRect.height)
<script src="http://d3js.org/d3.v5.js"></script>
<div class="svgLegend"></div>
Using getBBox()is a good idea. This is how I would do it.
let bbox = test.getBBox();
//console.log(bbox)
svg.setAttributeNS(null, "viewBox", `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height} `)
svg.setAttributeNS(null, "width", bbox.width);
svg.setAttributeNS(null, "height", bbox.height);
<svg id="svg" >
<g id="gtest">
<path id="test" d="M187.476,214.443c-2.566,11.574-4.541,22.658-7.542,33.456
c-3.558,12.8-7.14,25.713-12.242,37.938c-10.223,24.495-41.321,29.239-58.824,9.548c-9.592-10.792-11.295-26.9-3.539-40.556
c11.233-19.778,25.391-37.46,40.447-54.438c1.07-1.207,2.116-2.436,3.893-4.484c-7.212,0.9-13.349,1.988-19.529,2.374
c-16.283,1.018-32.578,2.21-48.881,2.437c-18.686,0.261-32.846-10.154-37.071-26.055c-6.762-25.449,15.666-48.973,41.418-43.338
c23.645,5.175,46.447,12.901,68.424,23.051c1.033,0.478,2.083,0.918,3.933,1.731c-0.83-1.947-1.341-3.225-1.911-4.475
c-9.896-21.701-18.159-43.986-23.192-67.337c-4.587-21.28,8.933-40.56,29.946-43.257c20.134-2.585,38.124,12.991,39.091,34.294
c1.029,22.682-0.049,45.292-3.58,67.755c-0.17,1.079-0.152,2.188-0.246,3.659c8.05-6.831,15.471-13.737,23.52-19.811
c11.147-8.412,22.398-16.795,34.27-24.113c18.35-11.312,40.821-4.481,50.028,14.385c9.091,18.628,0.131,40.586-20.065,48.198
c-11.034,4.158-22.248,7.944-33.594,11.143c-11.321,3.191-22.908,5.438-34.866,8.212c1.189,0.81,2.19,1.504,3.205,2.18
c18.402,12.261,37.157,24.032,55.101,36.932c14.769,10.616,18.619,29.317,10.675,44.578c-7.537,14.477-25.151,22.136-40.767,17.583
c-7.583-2.212-14.022-6.469-18.523-12.919c-12.463-17.86-24.638-35.924-36.898-53.925
C189.24,217.849,188.547,216.357,187.476,214.443z"/>
</g>
</svg>
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.
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; });
Another d3 newbie question here.
I am trying to transition change a donut chart with grouped nested data. Here's what I have now.
http://bricbracs.com/test/
So when I click on a segment arc like New York it will update with data from the dept column with a nested function so I get this. I am close. I have the data grouped. I need help redrawing the donut.
http://bricbracs.com/test1/
Here is a csv file.
status,dept,city,points
temp,finance,New York,33
contract,HR,London,12
contract,HR,New York,11
casual,shop,London,43
contract,shop,Paris,51
temp,finance,London,7
contract,office,New York,61
contract,shop,London,31
temp,office,New York,16
contract,office,London,19
temp,finance,London,7
contract,office,New York,61
contract,sales,London,31
temp,finance,New York,16
contract,sales,Paris,19
Here is the d3 script. Thanks in advance.
<script>
var width = 960,
height = 500,
radius = Math.min(width, height) / 2;
var color = d3.scale.category20();
var arc = d3.svg.arc()
.outerRadius(radius - 10)
.innerRadius(radius - 70);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) {
return d.values;
});
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
svg.style("cursor","pointer")
d3.csv("data.csv", function(error, data) {
var data = d3.nest()
.key(function(d) {
return d.city;
})
.rollup(function(d) {
return d3.sum(d, function(g) {
return g.points;
});
}).entries(data);
var g = svg.selectAll(".arc")
.data(pie(data))
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", arc)
.attr("stroke", "white")
.attr("stroke-width", 0.5)
.style("fill", function(d) {
return color(d.data.key);
})
.on("mouseover", function (d) {
d3.select("#tooltip")
.style("left", d3.event.pageX + "px")
.style("top", d3.event.pageY + "px")
.style("opacity", .75)
.select("#value")
.text(d.value.toLocaleString())
document.getElementById("demo").innerHTML =d.data.key
})
.on("mouseout", function () {
d3.select("#tooltip")
.style("opacity", 0);
console.log("OUT")
})
.on("mousemove", function () {
d3.select("#tooltip")
.style("left", (d3.event.pageX +20) + "px")
.style("top", d3.event.pageY + "px+50")
})
.on("click", function() {
change()
});
g.append("text")
.attr("transform", function(d) {
var c = arc.centroid(d),
x = c[0],
y = c[1],l
h = Math.sqrt(x*x + y*y);
return "translate(" + arc.centroid(d) + ")";
})
//.attr("dy", "1em")
.style("text-anchor", "middle")
.text(function(d) {
return d.data.key
})
function change() {
var data = d3.nest()
.key(function(d) {
return d.dept;
})
.rollup(function(d) {
return d3.sum(d, function(g) {
return g.points;
});
}).entries(data);
var path = svg.selectAll("path");
path = path.data(pie(data), function(d) { return d.data.key; })
path.enter().append("path").attr("fill", function(d) {return color(d.data.key); })
path.exit().remove()
path.attr("d", arc)
}
});