D3 - Legend in seperate div element - d3.js

I have a plunker here - https://plnkr.co/edit/inW1NfaUwechJt8C1hC9?p=preview
I'm trying to create a legend for this graph
I can do it by adding the legend to the svg but Id like more control over the styling and position
Is it possible to create the legend in a separate div element and use something like a ul list.
var legend = d3.select(".legend")
.data(colors)
.enter()
.append("g")
.attr("class", "legend")
.attr("transform", (d, i) => {
return "translate(20," + i * 35 + ")";
});

Yes, Based on your comment it appears as though a html list approach is what you want, I'll answer for that approach.
Rather than appending a g for each legend item, append li to a list. You can also append the div and ul with d3, just as any svg component. The only major change is appending the right type of element and setting its relevant properties - as this will be different with html than svg.
As with svg components, you can style with css or .attr/.style methods (I use both below).
Here's an example:
var data = [{"name":"Category 1", "value":1},{"name":"Category 2", "value":2},{"name":"Category 3", "value":3},{"name":"Category 4", "value":4},{"name":"Category 5", "value":5}];
var scale = d3.scaleLinear()
.range(["red","orange"])
.domain([1,5]);
var divLegend = d3.select("#divLegend");
divLegend.append("p")
.html("Legend Title")
.style("text-align","center")
.style("font-size","20px")
var list = divLegend.append("ul");
var entries = list.selectAll("li")
.data(data)
.enter()
.append("li");
// append rectangle and text:
entries.append("span")
.attr("class","rect")
.style("background-color", function(d) { return scale(d.value); })
entries.append("span")
.attr("class","label")
.html(function(d) { return d.name; })
ul {
list-style-type: none;
}
.rect {
width: 10px;
height: 10px;
display: inline-block;
}
.label {
margin-left: 10px;
}
#divLegend {
width: 150px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<div id="divLegend"></div>

Related

d3 v4 - can't scroll vertically on svg containing bar chart

EDIT - added a code snippet (bottom)
The amount of rows are dynamic, so I don't know what the total height needed is. I have initiated the height as 800px, but the list can grow longer. I have a container div with an overflow-y:scroll value, but that did not solve the issue. D3 code and css below.
There is a scroll bar, but if I scroll down no more rows show.
The chart itself only measures about 644px (in Photoshop), so I'm not sure why it is cutting off where it does, or if that is a separate issue.
$(document).ready(function() {
let randProb = () => {
let min = 0;
let max = 1;
return (Math.random() * (max - min) + min).toFixed(4)
};
var data = [];
let initData = () => {
for (let i=0; i<86; ++i) {
data.push({
"attribute":`ATTRIBUTE_${i}`,
"probabilityOfMastery":randProb()
});
}
// console.log('data initialized', data);
};
initData();
// set the dimensions and margins of the graph
var margin = {top: 0, right: 20, bottom: 0, left: 160},
width = 1220 - margin.left - margin.right,
height = 800 - margin.top - margin.bottom;
// set the ranges
var y = d3.scaleBand()
.range([height, 0])
.padding(0.1);
var x = d3.scaleLinear()
.range([0, width]);
// append the svg object to the body of the page
// append a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select(".analyticsContainer").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 + ")");
// format the data
data.forEach(function(d) {
d.probabilityOfMastery = +d.probabilityOfMastery;
});
// Scale the range of the data in the domains
x.domain([0, 1])
y.domain(data.map(function(d) { return d.attribute; }));
//y.domain([0, d3.max(data, function(d) { return d.probabilityOfMastery; })]);
// append the rectangles for the bar chart
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("width", 0 )
.attr("y", function(d) { return y(d.attribute); })
.transition()
.duration(1000)
//- .delay(function(d, i) {
//- return i * 60
//- })
.attr("height", y.bandwidth())
.attr("width", function(d) {return x(d.probabilityOfMastery); } )
;
// add the x Axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// add the y Axis
svg.append("g")
.call(d3.axisLeft(y));
});
.analyticsWrapper {
height:calc(100vh - 120px);
max-height:100%;
width:100vw;
max-width: 100%;
display:flex;
flex-direction: column;
justify-content: center;
align-items: center;
border:1px solid red
}
.analyticsContainer {
font-size: 40px;
width:100%;
height:100%;
justify-content: center;
overflow-y: scroll;
padding:3.5vw;
height:800px;
}
svg {
width:90vw;
max-height: 100%;
max-width: 100%;
overflow: scroll;
}
.bar {
fill: #11b9b9;
}
.yAxis text {
font-weight: 700;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<body>
<div class="analyticsWrapper">
<div class="analyticsContainer">
</div>
</div>
</body>

D3 simple drop down menu on click

I cant find a straight forward answer anywhere. How can I make a drop down menu appear on click of an svg circle? I dont want it on display until the shape has been clicked. Can someone help me please?
Download this and include in your html : https://github.com/patorjk/d3-context-menu
And to use on click of a circle :
circle.on('contextmenu', d3.contextMenu(menu)); // attach menu to element
Thats if you want plugins :)
EDIT
NO PLUGINS
If you don't want plugins just create a list of things you want to show, prevent the default behaviour and on right click show the created list and click anywhere else hide it.
Here is a fiddle (not mine) : http://jsfiddle.net/thatoneguy/u2kJq/727/
List he creates :
<ul class='custom-menu'>
<li data-action = "first">First thing</li>
<li data-action = "second">Second thing</li>
<li data-action = "third">Third thing</li>
</ul>
This will show on right click. Now to implement this with D3 you can use event.preventDefault(); like the fiddle above, you have to use d3.event.preventDefault();. Also you have to position the menu, so change the css top and left to position on mouse position :
css({
top: d3.event.pageY + "px",
left: d3.event.pageX + "px"
});
Here is a simple implementation of what I have described :
var w = 500;
var h = 50;
var dataset = [5, 10, 15, 20, 25];
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
var circles = svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle");
circles.attr("cx", function(d, i) {
return (i * 50) + 25;
})
.attr("cy", h / 2)
.attr("r", function(d) {
return d;
})
.on("contextmenu", function(event) {
// Avoid the real one
d3.event.preventDefault();
// Show contextmenu
$(".custom-menu").finish().toggle(100).
// In the right position (the mouse)
css({
top: d3.event.pageY + "px",
left: d3.event.pageX + "px"
});
});;
//$(document).bind
// If the document is clicked somewhere
$(document).bind("mousedown", function(e) {
// If the clicked element is not the menu
if (!$(e.target).parents(".custom-menu").length > 0) {
// Hide it
$(".custom-menu").hide(100);
}
});
// If the menu element is clicked
$(".custom-menu li").click(function() {
// This is the triggered action name
switch ($(this).attr("data-action")) {
// A case for each action. Your actions here
case "first":
alert("first");
break;
case "second":
alert("second");
break;
case "third":
alert("third");
break;
}
// Hide it AFTER the action was triggered
$(".custom-menu").hide(100);
});
.custom-menu {
display: none;
z-index: 1000;
position: absolute;
overflow: hidden;
border: 1px solid #CCC;
white-space: nowrap;
font-family: sans-serif;
background: #FFF;
color: #333;
border-radius: 5px;
}
.custom-menu li {
padding: 8px 12px;
cursor: pointer;
}
.custom-menu li:hover {
background-color: #DEF;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<body>
<ul class='custom-menu'>
<li data-action = "first">First thing</li>
<li data-action = "second">Second thing</li>
<li data-action = "third">Third thing</li>
</ul>
</body>
If you want to do something when clicking one of the menu items, you can use onclick in the html like so :
<li onclick='doSomething()' data-action = "third">Third thing</li>
Or you can just give the individual items an id like so :
<li id='listItemOne' data-action = "third">Third thing</li>
And then add event listener :
document.getElementById('listItemOne').addEventListener('click', doSomething)
#thisOneGuy gave a great answer, but since I've already coded it, here's a solution that builds the context menu with svg:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#3.4.6" data-semver="3.4.6" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.4.6/d3.min.js"></script>
<style>
svg {
font: 12px sans-serif;
}
</style>
</head>
<body>
<script>
var menu = [{
title: 'A really, really long item',
action: function(elem, d, i) {
console.log('Item #1 clicked!');
console.log('The data for this circle is: ' + d);
}
}, {
title: 'Item #2',
action: function(elem, d, i) {
console.log('You have clicked the second item!');
console.log('The data for this circle is: ' + d);
}
}]
var data = [1, 2, 3];
var g = d3.select('body').append('svg')
.on('click', function(){
m.style('display', 'none');
})
.attr('width', 500)
.attr('height', 400)
.append('g');
g.selectAll('circles')
.data(data)
.enter()
.append('circle')
.attr('r', 30)
.attr('fill', 'steelblue')
.attr('cx', function(d) {
return 100;
})
.attr('cy', function(d) {
return d * 100;
})
.on('contextmenu', function(d) {
var coors = d3.mouse(this);
m.attr('transform', 'translate(' + coors[0] + ',' + coors[1] + ')');
m.style('display', 'block');
m.datum(d);
d3.event.preventDefault();
});
/* build context menu */
var m = g.append("g")
m.style('display', 'none');
var r = m.append('rect')
.attr('height', menu.length * 25)
.style('fill', "#eee");
var t = m.selectAll('menu_item')
.data(menu)
.enter()
.append('g')
.attr('transform', function(d, i) {
return 'translate(' + 10 + ',' + ((i + 1) * 20) + ')';
})
.on('mouseover', function(d){
d3.select(this).style('fill', 'steelblue');
})
.on('mouseout', function(d){
d3.select(this).style('fill', 'black');
})
.on('click', function(d,i){
d.action(d, d3.select(this.parentNode).datum(), i);
})
.append('text')
.text(function(d) {
return d.title;
});
var w = 0;
t.each(function(d){
var l = this.getComputedTextLength();
if (l > w) w = l;
})
r.attr('width', w + 20);
</script>
</body>
</html>

Binding on click events to context menu list elements

I am generating dynamic context menus that appear when the user right clicks on a shape. I have managed to create the context menu, but I am having trouble catching the click event when the user selects an entry from the menu.
The event keeps binding to the right click action to create the context menu instead of a left click on the list items within the menu itself.
I have dug around SO a bunch and been unable to unearth something that will get me the rest of the way.
What I want is for the console.log function in this example to trigger when the user clicks a list element and pass the name of the item clicked. In a perfect world it would not trigger on the initial right click, but I'll take what I can get.
<!DOCTYPE html>
<html>
<head>
<script src="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<style>
.context-menu {
position: absolute;
display: none;
background-color: #f2f2f2;
border-radius: 4px;
font-family: Arial, sans-serif;
font-size: 14px;
min-width: 150px;
border: 1px solid #d4d4d4;
z-index:1200;
}
.context-menu ul {
list-style-type: none;
margin: 4px 0px;
padding: 0px;
cursor: default;
}
.context-menu ul li {
padding: 4px 16px;
}
.context-menu ul li:hover {
background-color: #4677f8;
color: #fefefe;
}
</style>
<script>
var fruits = ["Apple", "Orange", "Banana", "Grape"];
var svgContainer = d3.select("body")
.append("svg")
.attr("width", 200)
.attr("height", 200);
var circle = svgContainer
.append("circle")
.attr("cx", 30)
.attr("cy", 30)
.attr("r", 20)
.on('contextmenu', function(d,i) {
// create the div element that will hold the context menu
d3.selectAll('.context-menu').data([1])
.enter()
.append('div')
.attr('class', 'context-menu');
// close menu
d3.select('body').on('click.context-menu', function() {
d3.select('.context-menu').style('display', 'none');
});
// this gets executed when a contextmenu event occurs
d3.selectAll('.context-menu')
.html('')
.append('ul')
.selectAll('li')
.data(fruits).enter()
.append('li')
// THIS IS WHAT I CAN NOT GET TO WORK THE WAY I WANT IT TO WORK
.on('click' , console.log( function(d) { return d; } + " clicked!"))
.text(function(d) { return d; });
d3.select('.context-menu').style('display', 'none');
// show the context menu
d3.select('.context-menu')
.style('left', (d3.event.pageX - 2) + 'px')
.style('top', (d3.event.pageY - 2) + 'px')
.style('display', 'block');
d3.event.preventDefault();
});
</script>
</body>
</html>
Here is a plunkr demonstrating the code (I can't figure out why I couldn't get it to run with jsfiddle): http://run.plnkr.co/plunks/paPKKlUFtQCGpOmjQztS/
Please see this fiddle
I basically updated your click listener to this:
.on('click' , function(d) { console.log(d); return d; })
It seems to be working fine.

Add links and hover effects to multi series donut chart in d3.js

I have a multi series donut chart created with the help of this question D3.js - Donut charts with multiple rings in d3.js, see fiddle below.
I'd like to be able to add hover effects, and also make each part clickable in the sense I'd like to assign a certin href to each slice of the chart. I have looked around quite a bit, but can't get my head around it - d3.js is quite complex for me I guess.
The code I have now: http://jsfiddle.net/mephisto73/o6shxw0d/
(function(){
var $container = $('.chart-container'),
τ = 2 * Math.PI,
width = $container.width(),
height = $container.height(),
outerRadius = Math.min(width,height)/2.5,
innerRadius = (outerRadius/5)*4,
fontSize = (Math.min(width,height)/4);
var dataset = {
weeks: [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
months: [1,1,1,1,1,1,1,1,1],
trimester: [1,1,1]
};
var color = d3.scale.ordinal() .range(['rgba(141,211,199,0.8)','rgb(255,255,179)','rgb(190,186,218)','rgb(251,128,114)','rgb(128,177,211)','rgb(253,180,98)','rgb(179,222,105)','rgb(252,205,229)','rgb(217,217,217)','rgb(188,128,189)','rgb(204,235,197)','rgb(255,237,111)']);
var pie = d3.layout.pie()
.sort(null);
var arc = d3.svg.arc();
var svg = d3.select('.chart-container').append("svg")
.attr("width", '100%')
.attr("height", '100%')
.attr('viewBox','0 0 '+Math.min(width,height) +' '+Math.min(width,height) )
.attr('preserveAspectRatio','xMinYMin')
.append("g")
.attr("transform", "translate(" + Math.min(width,height) / 2 + "," + Math.min(width,height) / 2 + ")");
var gs = svg.selectAll("g").data(d3.values(dataset)).enter().append("g").attr("class", "arc");
var path = gs.selectAll("path")
.data(function(d) { return pie(d); })
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", function(d, i, j) { return arc.innerRadius(innerRadius+(40*j)).outerRadius(innerRadius+(5*(j+5)))(d); });
});
$(function(){
var tooltip = d3.select(".tooltip");
var $container = $('.chart-container'),
τ = 2 * Math.PI,
width = $container.width(),
height = $container.height(),
outerRadius = Math.min(width,height)/2.5,
innerRadius = (outerRadius/5)*4,
fontSize = (Math.min(width,height)/4);
var dataset = {
weeks: [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
months: [1,1,1,1,1,1,1,1,1],
trimester: [1,1,1]
};
var color = d3.scale.ordinal() .range(['rgba(141,211,199,0.8)','rgb(255,255,179)','rgb(190,186,218)','rgb(251,128,114)','rgb(128,177,211)','rgb(253,180,98)','rgb(179,222,105)','rgb(252,205,229)','rgb(217,217,217)','rgb(188,128,189)','rgb(204,235,197)','rgb(255,237,111)']);
var pie = d3.layout.pie()
.sort(null);
var arc = d3.svg.arc();
var svg = d3.select('.chart-container').append("svg")
.attr("width", '100%')
.attr("height", '100%')
.attr('viewBox','0 0 '+Math.min(width,height) +' '+Math.min(width,height) )
.attr('preserveAspectRatio','xMinYMin')
.append("g")
.attr("transform", "translate(" + Math.min(width,height) / 2 + "," + Math.min(width,height) / 2 + ")");
var gs = svg.selectAll("g").data(d3.values(dataset)).enter().append("g").attr("class", "arc");
var path = gs.selectAll("path")
.data(function(d) { return pie(d); })
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", function(d, i, j) { return arc.innerRadius(innerRadius+(40*j)).outerRadius(innerRadius+(5*(j+5)))(d); })
.on("mousemove", function(d){
tooltip.style("left", d3.event.pageX+10+"px");
tooltip.style("top", d3.event.pageY-25+"px");
tooltip.style("display", "inline-block");
tooltip.select("span").text("Value: "+d.value);
}).on("mouseout",function(){
tooltip.style("display","none");
}).on("click",function(){
//write code to open
});
});
html,body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
margin:0;
padding:0;
width:100%;
height:100%;
}
text {
font: 10px sans-serif;
}
form {
position: absolute;
right: 10px;
top: 10px;
}
.arc path {
stroke: #fff;
width:5px;
}
.arc path:hover {
background-color:#ccc;
}
.chart-container {
width:70%;
height:70%;
border: 1px dotted silver;
}
svg text{
font-size: 1em;
font-family: sans-serif;
}
.tooltip{
position: absolute;
display: none;
width: auto;
height: auto;
background: none repeat scroll 0 0 white;
border: 0 none;
border-radius: 8px 8px 8px 8px;
box-shadow: -3px 3px 15px #888888;
color: black;
font: 12px sans-serif;
padding: 5px;
text-align: center;
}
path:hover{
cursor: pointer;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.10/d3.min.js"></script>
<div class="chart-container"></div>
<div class='tooltip'>
<span></span>
</div>
I've added functionality for on mousemove, mouseout and click.
Try to read and do the modification in click function.
Hope you got it,If not ask me for more.

How to adjust svg height within table cell?

First time using jsfiddle not too successful - jsfiddle.net/mic1/o9f18aLz/1/ (appreciate pointers here)
I need to place up to 5 charts vertically within a container where how many charts is not known until runtime. I decided a table/row/cell/div structure would be flexible ( is there a better design?)
<style type="text/css">
.element {
width: 120px;
height: 200px;
box-shadow: 0px 0px 12px rgba(0,255,255,0.5);
border: 1px solid rgba(127,255,255,0.25);
text-align: center;
cursor: default;
}
}
.tile-table {
/*table-layout:fixed;*/
width: 100%;
height: 100%;
border: 3px solid orange;
}
</style>
var tableSet1 = [ 33.3, 33.3, 33.3 ];
var div1 = d3.select("body").append("div")
.classed("element", true)
.append("table")
.classed("tile-table", true)
.selectAll("tr")
.data(tableSet1)
.enter()
.append("tr")
.style("height", function(d) {return d + '%'})
.style("background-color", "yellow")
.append("td")
.style("height", function(d) {return d + '%'})
.append(function(d) {return createChart1(d)});
function createChart1(d) {
var topChart = document.createElement( 'div' );
var svg = d3.select(topChart)
.append("svg")
.style("background-color", "lime")
.attr("height", function() {
return 20}) // using d or 100 + '% no good
.attr('width', function() {
return 100 +'%'});
return topChart;
}
Once I have the dataset - 3 charts at 33.3% height each, the svg for each chart should fill the height of each table row cell. I have it set at a bogus 20px here. 100 + % seems to set it to 100% of the top parent div, so would like to know what I should be manipulating here. I do have the percentage as d (e.g. 33.3) available in the called function.
Oh, this is a mess!
The first thing to know is that the SVG's aren't being set to the height of the table, they are being set to 150px, which is the "default" height for SVG if you don't set a valid height. All the values you are setting as percents are being treated as invalid.
Normally, setting an element's height using a percentage value would be valid iff (if and only if) the parent element has a defined height. Since you're setting the height of the <td> and <tr> elements, that should be straightforward -- but it's not.
CSS height and width properties work differently inside tables. The heights you set with the height property on the row and cell are actually treated as minimum heights for the row, and the cell's height is always dependent on the height required for its children. In other words, the cell never has a fixed height, and therefore percentage heights for its children never have meaning.
In the snippet, I've re-arranged your code to create multiple versions of the table, each with different settings for the SVG height. As you can see, 100%, 33% and null values all result in the SVGs being set to 150px tall, and the tables stretching to fit:
var tableSet1 = [ 33.3, 33.3, 33.3 ];
var div1 = d3.select("body").selectAll("div")
.data(["100%", "33%", "30px", null])
.enter().append("div")
.classed("element", true)
.each(function(option){
d3.select(this)
.append("table")
.classed("tile-table", true)
.selectAll("tr")
.data(tableSet1)
.enter()
.append("tr")
.style("height", function(d) {return d + '%'})
.style("background-color", "gray")
.append("td")
//.style("height", function(d) {return 100 + '%'})
.append(function(d) {return createChart1(d, option)});
});
function createChart1(d, option) {
var topChart = document.createElement( 'div' );
var svg = d3.select(topChart)
.append("svg")
.style("background-color", "seagreen")
.attr("height", function() {
return option})
.attr('width', function() {
return 100 +'%'});
return topChart;
}
.element {
width: 60px;
height: 200px;
box-shadow: 0px 0px 20px rgba(0,155,255,0.75);
border: 1px solid rgba(127,255,255,0.25);
text-align: center;
cursor: default;
display:inline-block;
margin:0 10px;
}
.tile-table {
/*table-layout:fixed;*/
width: 100%;
height: 100%;
border: 3px solid orange;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
So, what can you do?
Don't use tables. Tables are really only useful when you have multiple columns, and you want elements in each column to line up nicely. If you're just going to have one column, you can use <div> elements.
If you do really need a table structure, calculate the height of the SVG elements yourself by accessing the height of the parent div and then applying your percentage, and subtracting for borders and padding.

Resources