Related
I want my bar graph to have different coloured versions of the same icon for the legend tiles. I am currently using font-awesome 5 for text in my HTML document but I do not mind using other icon libraries if I must (as long as it's free). At the moment my graph has squares for the legend tiles as that is the default. In font-awesome the icon I want is class="fa fa-bar-chart". The class for the legend tile is called .c3-legend-item-tile
I tried code from Use Font Awesome Icons in CSS but the code there didn't help change the legend tiles. Neither did Using Font Awesome icon for bullet points, with a single list item element
I don't want my icons to float above the bar chart like the example from Adding icons to bar charts made using c3js I just want the tiles to change from the default square to an icon.
https://jsfiddle.net/SharonM/k49gbs13/25/ has a rough example of what I've tried. (Labels show what I want the actual tiles to look like but I do not actually want labels)
.c3-chart-text .c3-text {
font-family: 'FontAwesome';
}
.c3-legend-item-tile::before {
font-family: 'FontAwesome';
content: '\uf080'!important;
font-weight: 900!important;
margin: 0 5px 0 -15px!important;
}
I've tried before and after and also just the class itself.
Update:
I also tried:
d3.select(string).insert('div', '.chart').attr('class', 'legend').selectAll('span')
.data(this.allIDs)
.enter().append('span')
.attr('data-id', function (id) { return id; })
.html(function (id) { return '<i class="fa fa-bar-chart" aria-hidden="true"> </i>'+ id; })
.each(function (id) {
d3.select(this).style('background-color', chart.color(id));
})
.on('mouseover', function (id) {
chart.focus(id);
})
.on('mouseout', function (id) {
chart.revert();
})
.on('click', function (id) {
chart.toggle(id);
});
where string is the name of my container class but that did not do what I wanted at all. It created new 'legends' on the side with the icon I wanted but that bypassed my onclick checks when toggling which I could re-implement in this function but it just looks really bad. I'd rather the original little square was replaced by the icon.
Version for C3 < 0.7
It turns out the way to do this and keep the colours is to do stuff to the .c3-legend-item-tile(s). Since they're rectangles you can't add text to them but what you can do is use masks and patterns on them to give the impression of text.
My first attempt just replaced the fill style of the c3-legend-item-tiles with a pattern, where the pattern was the text character needed. However, this removed the colour and just showed them as black - not very handy
It turns out what you can do though is add a mask separately from the fill style and reuse the pattern within there --> How to change color of SVG pattern on usage? . Here we set the mask to be a rectangle, which in turn uses the pattern as a fill, and hey presto the 'icons' appear in the right colour as the fill style per tile stays as it was...
https://jsfiddle.net/j2596x0u/
var chart = c3.generate({
data: {
columns: [
['data1', 30, -200, -100, 400, 150, 250],
['data2', -50, 150, -150, 150, -50, -150],
['data3', -100, 100, -40, 100, -150, -50]
],
type: 'bar',
labels: {
// format: function (v, id, i, j) { return "Default Format"; },
format: {
data1: function (v, id, i, j) { return "\uf080"; },
data2: function (v, id, i, j) { return "\uf080"; },
data3: function (v, id, i, j) { return "\uf080"; },
}
}
},
grid: {
y: {
lines: [{value: 0}]
}
},
onrendered: function () {
d3.selectAll(".c3-legend-item-tile").attr("mask", "url(#iconMask)");
}
});
d3.select("svg defs").append("mask")
.attr("id", "iconMask")
.attr("x", 0)
.attr("y", 0)
.attr("width", 1)
.attr("height", 1)
.attr("maskContentUnits", "objectBoundingBox")
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 1)
.attr("height", 1)
.style("fill", "url(#iconPattern)")
;
d3.select("svg defs").append("pattern")
.attr("id", "iconPattern")
.attr("x", 0)
.attr("y", 0)
.attr("width", 1)
.attr("height", 1)
.style("fill", "#fff")
.attr("patternContentUnits", "objectBoundingBox")
.append("text")
.attr("x", "0.2em")
.attr("dy", "1em")
.attr("font-size", 0.8)
.attr ("font-family", "FontAwesome")
.text("\uf080")
;
I won't pretend it was simple, I had to use objectBoundingBox and normalise everything in the masks and patterns to between 0 and 1 to get them to show up in the right place from the example I found, and the font-size was trial and error. But, yeh, it's doable.
C3 0.7+
Ha, it turns out the mask didn't work simply because the dom considers the lines to have zero height even though it is drawn with 10px thickness. If I change the y2 attribute so the line is technically a diagonal there's enough space for the text mask and pattern to render:
For c3 versions where .c3-legend-item-tile's are lines rather than rects
onrendered: function () {
d3.selectAll(".c3-legend-item-tile").attr("mask", "url(#iconMask)")
.each (function (d,i) {
var d3this = d3.select(this);
d3this.attr("y2", +d3this.attr("y1") + 10);
})
;
}
https://jsfiddle.net/u7coz2p5/3/
I am having a great deal of trouble getting my pie/donut chart to update data dynamically. I have it configured so that the user slides a range input to select which month of data he/she wants to see, then the data is passed to my d3 visual. For the sake of simplicity I have hard-coded the data in my example. You may view the snippet below:
var width = 450;
var height = 350;
var margins = { left: 0, top: 0, right: 0, bottom: 0 };
var svg = d3.select("body")
.append("svg")
.attr("width", width+margins.right)
.attr("height", height+margins.top);
var period = ['JAN 2016','FEB 2016','MAR 2016', 'APR 2016', 'MAY 2016', 'JUN 2016',
'JUL 2016', 'AUG 2016', 'SEP 2016', 'OCT 2016', 'NOV 2016', 'DEC 2016'];
d3.select('#timeslide').on('input', function() {
update(+this.value);
});
function update(value) {
document.getElementById('range').innerHTML=period[value];
create_pie(period[value]);
}
var pie_data = {
'JAN2016': [16,4,1,30],
'FEB2016': [17,4,0,30],
'MAR2016': [16,5,1,29],
'APR2016': [17,4,1,29],
'MAY2016': [17,4,1,29],
'JUN2016': [17,4,2,28],
'JUL2016': [18,3,2,28],
'AUG2016': [18,3,2,28],
'SEP2016': [18,3,2,28],
'OCT2016': [18,3,3,27],
'NOV2016': [18,3,3,27],
'DEC2016': [18,3,3,27]
}
function create_pie(month) {
var radius = Math.min(width, height) / 4;
var color = d3.scale.ordinal().range(['darkblue','steelblue','blue', 'lightblue']);
var arc = d3.svg.arc()
.innerRadius(radius - 50)
.outerRadius(radius - 20);
var sMonth = String(month).replace(' ','');
var pData = pie_data[sMonth];
var pie = d3.layout.pie()
.padAngle(.05)
.value(function(d) { return d; })
.sort(null);
var pieG = svg.append('g')
.attr('transform', 'translate(' + 100 +',' + 75 + ')');
var Ppath = pieG.datum(pData).selectAll(".pie")
.data(pie);
Ppath
.enter().append("path").attr('class','pie');
Ppath
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) {
this.x0 = d.x;
this.dx0 = d.dx;
});
Ppath
.transition()
.duration(650)
.attrTween("d", arcTweenUpdate);
Ppath
.exit().remove();
function arcTweenUpdate(a) {
var i = d3.interpolate({x: this.x0, dx: this.dx0}, a);
return function(t) {
var b = i(t);
this.x0 = b.x;
this.dx0 = b.dx;
return arc(b);
};
}
}
create_pie('JAN 2016');
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script src="//d3js.org/d3.v3.min.js"></script>
</head>
<body>
<div id='sliderContainer'>
<input id='timeslide' type='range' min='0' max='11' value='0' step='1'/><br>
<span id='range'>JAN 2016</span>
</div>
The good news is the pie chart is getting the new data each time the month is updated, because it looks like the pie chart is indeed moving. The bad news is the pie chart is looking very jerky and it seems my .transition().duration(650) is not working at all. Actually I am started to think that the pie chart is being drawn again and again on top of itself, because the pie chart looks more blurry with each update of the data. I'm not sure why that would be since I was extra careful to include the Ppath.exit().remove(); at the presumably correct place. Ultimately, I'm left feeling like my understanding of how to dynamically update pie data is fundamentally flawed.
I soon realized I wasn't the first to have some issues with the pie transitions. The trickiest thing seems to be the arcTween part. I looked at http://stackoverflow.com/questions/22312943/d3-sunburst-transition-given-updated-data-trying-to-animate-not-snap and followed along as closely as I could. I used that implementation of the arcTweenUpdate, but unfortunately my situation did not improve. You may notice from the snippet that the colored arcs are moving around but the empty spaces that "divide" or "slice" up the pie are static. That's not what I want, it should all be transitioning, nice and smooth. There should not be static parts or awkwardly transitioning parts like it is currently.
Question: How can I keep the dynamic nature of the visual (access pie_data in its current format via my function create_pie) but also have a smooth, clean looking transition like M. Bostock's classic donut?
Note: M. Bostock's block uses a change() function to update the pie chart. I prefer an answer that corrects/augments/adds to my existing code structure (i.e. Ppath.enter()... Ppath.transition() ... Ppath.exit().remove()) However, I would be willing to accept a change() function similar to M. Bostock's orginial if someone can explain explicitly why my method as per this post is impossible / highly impracticle.
Edit
Another unforseen issue when I try to update the data dynamically is concerning radio buttons. As per Karan3112's formatData() function:
function formatData(data) {
if (document.getElementById("radioButton1").checked) {
return data[Object.keys(data)[0]].slice(0,4).map(function(item, index) {
let obj = {};
Object.keys(data).forEach(function(key) {
obj[key] = data[key][index] //JAN2016 : [index]
})
return obj;
})
}
else if (document.getElementById("radioButton2").checked) {
return data[Object.keys(data)[0]].slice(5,8).map(function(item, index) {
let obj = {};
Object.keys(data).forEach(function(key) {
obj[key] = data[key][index] //JAN2016 : [index]
})
return obj;
})
}
}
Basically, in my real data, each month has an array of length 8 like this:
'JAN2016': [17,4,1,29,7,1,1,42],
So depending on which radio button is checked, I want to have the pie be drawn according to either the first 4 items in the array for radioButton1 or the last 4 items in the array for radioButton2.
I initially omitted this part of the task for my OP because I figured it would be simple enough to adapt, but after trying for a good while with little progress, I have reconsidered. My slices don't seem to be working. I think it is because the formatData function is only called once. I tried putting a formatData call inside the change() function, but that didnt work either.
Following the example by Mike Bostock have updated your code as follows.
Added a data format function which will return the data in a {label : value} format.
Updated the code logic from loading/redrawing the data onUpdate to updating the pie value.
var width = 450;
height = 350;
radius = Math.min(width, height) / 4;
var color = d3.scale.ordinal().range(['darkblue','steelblue','blue', 'lightblue']);
var pie = d3.layout.pie()
.value(function(d) { return d['JAN2016']; })
.sort(null);
var arc = d3.svg.arc()
.innerRadius(radius - 50)
.outerRadius(radius - 20);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr('transform', 'translate(' + 100 +',' + 75 + ')');
var period = ['JAN 2016','FEB 2016','MAR 2016', 'APR 2016', 'MAY 2016', 'JUN 2016',
'JUL 2016', 'AUG 2016', 'SEP 2016', 'OCT 2016', 'NOV 2016', 'DEC 2016'];
var pie_data = {
'JAN2016': [16,4,1,30],
'FEB2016': [17,4,0,30],
'MAR2016': [16,5,1,29],
'APR2016': [17,4,1,29],
'MAY2016': [17,4,1,29],
'JUN2016': [17,4,2,28],
'JUL2016': [18,3,2,28],
'AUG2016': [18,3,2,28],
'SEP2016': [18,3,2,28],
'OCT2016': [18,3,3,27],
'NOV2016': [18,3,3,27],
'DEC2016': [18,3,3,27]
};
var path = svg.datum(formatData(pie_data)).selectAll("path")
.data(pie)
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) { this._current = d; }); // store the initial angles
d3.select('#timeslide').on('input', function() {
change(this.value);
});
function change(key) {
var value = period[key].replace(' ','');
document.getElementById('range').innerHTML=period[key];
pie.value(function(d) { return d[value]; }); // change the value function
path = path.data(pie); // compute the new angles
path.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
}
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
function formatData(data){
return data[Object.keys(data)[0]].map(function(item, index){
let obj = {};
Object.keys(data).forEach(function(key){
obj[key] = data[key][index] //JAN2016 : [index]
})
return obj;
})
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<div id='sliderContainer'>
<input id='timeslide' type='range' min='0' max='11' value='0' step='1'/><br>
<span id='range'>JAN 2016</span>
</div>
I am trying to create a scatter plot with unequal intervals on the X-axis using d3.js. My CSV data is shown here partially:
chr,pos,val
22,8947,8.58891099252
22,8978,4.65541559632
22,8996,6.33685790218
22,8997,9.00384002282
22,9006,4.39533823989
MT,9471,5.0655064583
MT,9472,7.83798949399
MT,9473,0.587797595352
MT,9474,4.6475160648
MT,9475,2.52382097771
MT,9476,7.8431366396
MT,9477,1.71519736769
MT,9478,2.61168595179
MT,9479,4.15061022346
MT,9470,7.1477707428
The number of pos values for each chr value may be different. In some cases, it could be 20, in others 100 and so on. I need to create a plot of val on the y-axis vs chr on the x-axis, with the x-interval for each chr being equal to the number of pos values for that chr. Although ordinal scale for the x-axis seems suitable here, it probably doesn't support unequal intervals. With linear scale, unequal intervals can be shown using polylinear scales, but the presence of alphabetic characters in chr mean no ticks are shown. Does anyone know how I can show unequal intervals in d3.js?
UPDATE:
I have some code here for the domain and ticks using a linear scale:
const x = d3.scale.linear()
.domain(input.map((d) => {
if (d.chr === 'MT') {
return 23;
}
if (d.chr === 'X') {
return 24;
}
return d.chr;
}))
.range(xTicks);
I can't understand how to show the ticks now.With this it shows 23 and 24 instead of MT and X.
I am not sure of this part:
const xAxis = d3.svg.axis().scale(x).orient('bottom').tickValues(input.map((d) => {
if (d.chr === 'MT') {
// returning a string here shows NaN
return 23;
}
if (d.chr === 'X') {
return 24;
}
return d.chr;
}));
Here is an example of how conditionally formatting the ticks using tickFormat (not tickValues).
Suppose the data is:
19, 20, 21, 22, 23, 24;
But we are going to change 23 for "X" and 24 for "MT" in the ticks. Click "run code snippet":
var data = [19, 20, 21, 22, 23, 24];
var width = 400, height = 100;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("heigth", height);
var xScale = d3.scale.linear()
.domain(d3.extent(data))
.range([0, width*.9]);
var xAxis = d3.svg.axis()
.orient("bottom")
.ticks(6)
.tickFormat(function(d){
if(d == 23){
return "X"
} else if(d==24){
return "MT"
} else {
return d
}
})
.scale(xScale);
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(20,20)")
.call(xAxis);
.axis path,
.axis line {
fill: none;
stroke: #aaa;
shape-rendering: crispEdges;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I started at chromosome 19 just to save some space, but you can get the general idea.
I have a geoJSON looking like so
{"type":"FeatureCollection",
"crs":{"type":"name",
"properties":{"name":"urn:ogc:def:crs:OGC:1.3:CRS84"}},
"features":[{"type":"Feature",
"properties":{"scalerank":10,"natscale":1,"labelrank":8,"featurecla":"Admin-1 capital","name":"Colonia del Sacramento","namepar":null,"namealt":null,"diffascii":0,"nameascii":"Colonia del Sacramento","adm0cap":0,"capalt":0,"capin":null,"worldcity":0,"megacity":0,"sov0name":"Uruguay","sov_a3":"URY","adm0name":"Uruguay","adm0_a3":"URY","adm1name":"Colonia","iso_a2":"UY","note":null,"latitude":-34.479999,"longitude":-57.840002,"changed":4,"namediff":1,"diffnote":"Added missing admin-1 capital. Population from GeoNames.","pop_max":21714,"pop_min":21714,"pop_other":0,"rank_max":7,"rank_min":7,"geonameid":3443013,"meganame":null,"ls_name":null,"ls_match":0,"checkme":0},
"geometry":{"type":"Point","coordinates":[-57.8400024734013,-34.4799990054175]
}}]
}
I want to set to use colorbrewer to chose colors, depending on the value pop_max takes. Then I want to display this point data on a leaflet map through overlaying a svg ontop of leaflet. I can easily display the points and chose the color like so:
var feature = g.selectAll("path")
.data(collection.features)
.enter()
.append("path")
.style("fill", function(d) {
if(d.properties.pop_max) < 1000 {
return("red")
} else if {....
};
});
However, inconvenient.
So i tried:
var colorScale = d3.scale.quantize()
.range(colorbrewer.Greens[7])
.domain(0,30000000);
var feature = g.selectAll("path")
.data(collection.features)
.enter()
.append("path")
.style("fill", function(d) {
colorScale(d.properties.pop_max);
});
That does not display any points at all... Note that I estimated my domain. 0 is not necessarily the lowest number nor 30000000 the highest.
Any ideas?
First you'll need to find the max and min pop_max, something like this should work:
var extent = d3.extent(geojson.features, function(d) { return d.properties.pop_max; });
Second, since you want colors to represent 7 ranges of values you should be using d3.scale.threshold:
var N = 7,
step = (extent[1] - extent[0]) / (N + 1),
domain = d3.range(extent[0], extent[1] + step, step);
var colorScale = d3.scale.threshold()
.domain(domain)
.range(colorbrewer.Greens[N]);
EDITS
Looks like quantile can do this easier:
d3.scale.quantile()
.domain([extent[0], extent[1]])
.range(colorbrewer.Greens[N]);
I am trying to create an easy way to create and update donut charts (And other charts, later) in an application. This is built on top of D3.
I've created a drawDonutChart as shown below:
var data = [50, 50];
var dataTwo = [75, 25];
var options = {
colors: ["#0074D9", "#7FDBFF"],
};
function createDonutChart(data, height, width, domElement, options) {
var radius = Math.min(width, height);
var color = d3.scale.category20();
var donut = d3.layout.pie().sort(null);
var arc = d3.svg.arc()
.innerRadius(radius/4)
.outerRadius(radius/2);
var svg = d3.select(domElement).append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = svg.selectAll("path")
.data(donut(data))
.enter().append("path")
.attr("fill", function(d, i) {
if (options.colors == null || options.colors == "undefined") {
return color(i);
} else {
return options.colors[i];
}
}).attr("d", arc);
}
var donutChart = createDonutChart(data, 50, 50, ".slices", options);
when you have a in the page, this will draw a donut chart there.
I would like to write a function updateDonutChart(originalChart, newData), pass it (donutChart, dataTwo) and have it transition this data into the chart instead of the old data.
I've been looking at multiple examples on the D3 website but I haven't been able to get something to work this way (where you simply pass the old chart, and new data). It might be simple, I am just new to D3 :)
Thanks in advance for the help.
So, it wasn't clear to me from the signature of your update call if you wanted to create a new SVG under a possibly new DOM element every time you updated the chart. Also, I am not sure if you are using the word transition to specify a D3 transition of just a change in the data (although that would be simple enough to add). I any case, a simple adaptation to your code to do what you want is in this FIDDLE.
function updateDonutChart(data, options) {...