I've tried reading the documentation but I haven't found a simple way to add a suffix to a hover text.
Is there a way to add text to the hoverformat? For example y respondents where y is the y value.
The setting yaxis: {hoverformat: ''} does not seem to allow strings?
I'm used to working with FlotChart where you can simply put "%y respondents".
Any help is much appreciated.
If you set text in your trace you can get a suffix.
Sets text elements associated with each (x,y) pair. If a single
string, the same string appears over all the data points. If an array
of string, the items are mapped in order to the this trace's (x,y)
coordinates.
Together with hoverinfo (hoverinfo: 'text+y') you get close to what you what you want (except for the unneeded line break which would need to be removed manually).
For bar charts this does not seem to work (at least not in Jan 2017). It is necessary to replicate the data and write it into an array in the text attribute (which also solves the line break issue).
var N = 16,
x = Plotly.d3.range(N).map(Plotly.d3.random.normal(3, 1)),
y = Plotly.d3.range(N),
data = [{
x: x,
y: y,
type: 'scatter',
mode: 'markers',
text: 'respondents',
marker: {
size: 16
},
hoverinfo: 'text+y'
}],
layout = {
hovermode: 'closest',
};
Plotly.plot('myDiv', data, layout);
//here comes the bar chart
var data = [{
x: ['Jan', 'Feb', 'Mar'],
y: [20, 14, 25],
type: 'bar',
text: [],
hoverinfo: 'text'
}];
for (N = 0; N < data[0].y.length; N += 1) {
data[0].text.push(data[0].y[N] + ' Respondents');
}
Plotly.plot('myBarChart', data);
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<div id="myDiv" style="width:400px;height:400px;"></div>
<div id="myBarChart" style="width:400px;height:400px;"></div>
Related
Continue my journey with Plotly.js. I'm using relative barmode and need to show some customized text when both opposite bars have value 0. The issue that for value 0 both bars grow in the same direction and labels are overlapped:
The only solution which I've thought about is checking when the value is 0 and change it dynamically to something like -0.009 and manually display the right value despite it's actually wrong. But it's cumbersome solution and chart keep rendering tiny bar for those values which isn't acceptable.
This is my example on Codepen
Can label direction be controlled manually? Thank you.
Initial Thoughts
One solution is to specify the base parameter for each trace as 0 and for the negative bars use a negative base value if the value is 0.
const altbase=-1000/3;
const trace1 = {
x: xValue,
y: [FN],
name: `Model 1`,
text: `${FN} <br> FN`,
type: 'bar',
base: 0,
textposition: 'outside'
};
const trace2 = {
x: xValue,
y: [-TN],
name: 'Model 1',
text: `${TN} <br> TN`,
base: altbase,
type: 'bar',
textposition: 'outside'
};
It looks like -max_y_lim / 3 results in a good offset which is what altbase is used for.
You could also define altbase as a variable checking if TN==0:
var altbase = ((TN==0) ? -FP/3 : 0);
UPDATE1: or you can bypass the variable and put the if statement right into trace definition like base: ((TN==0) ? -FP/3 : 0),
UPDATE2: and a slightly more elegant equation for the offset would be:
var altbase = ((TN==0) ? -1.4/4*Math.max(FN,TN,TP,FP) : 0);
Final Update
After some thought it's probably best to always calculate altbase so that it's available to the other potential negative bar and then use an if statement for setting the base: where needed.
var altbase = -1.4/4*Math.max(FN,TN,TP,FP);
const trace1 = {
x: xValue,
y: [FN],
name: `Model 1`,
text: `${FN} <br> FN`,
type: 'bar',
base: 0,
textposition: 'outside'
};
const trace2 = {
x: xValue,
y: [-TN],
name: 'Model 1',
text: `${TN} <br> TN`,
base: ((TN==0) ? altbase : 0),
type: 'bar',
textposition: 'outside'
};
Here is the implementation in CodePen.
I am using a crossfilter2 with dcv3
My data is in a csv which i loaded into memory
Original Data
Day, ID
1, 2
1, 2
1, 2
2, 5
3, 6
4, 6
Processed data
Day, ID, target
1, 2, True
1, 2, True
1, 2, True
2, 5, False
3, 6, False
4, 6, False
Currently what i am trying to do is create a crossfilter stackedbar chart with 2 bars. If ID == 2, i consider it as one group, and ID !=2 as another group. However, i cannot do it dynamically it in DC/crossfilter which results me having to preprocess the data to add a new column and work off the column as shown by my solution below.
Is there a better way?
var dimID = ndx.dimension(function(d) { return d.day; });
var id_stacked = dimID.group().reduce(
function reduceAdd(p, v) {
p[v.target] = (p[v.target] || 0) + 1;
return p;
},
function reduceRemove(p, v) {
p[v.target] = (p[v.target] || 0) - 1;
return p;
},
function reduceInitial() {
return {};
});
//Doing the stacked bar chart here
stackedBarChart.width(1500)
.height(150)
.margins({top: 10, right: 10, bottom: 50, left: 40})
.dimension(dimID)
.group(id_stacked, 'Others', sel_stack("True"))
.stack(id_stacked, 'Eeid of interest', sel_stack("False"))
This is my sel_stack function
function sel_stack(i) {
return function(d) {
return d.value[i] ? d.value[i] : 0;
};
}
I am plotting a bar chart with x-axis being the day and the Y-axis being the frequency of ID == 2 or ID!=2 in a stacked bar chart
So you want to group by day and then stack by whether ID===2. Although dc.js will accept many different formats, often the trick is getting the data into the right shape.
You're on the right track, but you don't need the extra column in order to create stacks for "is 2" and "not 2". You can calculate it directly:
var dayDimension = ndx.dimension(function(d) { return d.Day; }),
idStackGroup = dayDimension.group().reduce(
function add(p, v) {
++p[v.ID===2 ? 'is2' : 'not2'];
return p;
},
function remove(p, v) {
--p[v.ID===2 ? 'is2' : 'not2'];
return p;
},
function init() {
return {is2: 0, not2: 0};
});
These are standard add/remove functions for reducing multiple values for each bin. You'll find other variations where the name of the field is driven by the data. But here we know what fields will exist, so we can initialize them to zero in init and not worry about encountering new fields.
The add function is called when a row is added to the crossfilter or a filter changes so that a row is included; the remove function is called whenever a row is filtered out or removed from crossfilter. Since we're not worried about undefined (1) we can simply increment (++) and decrement (--) the values.
Finally we need accessors to pull these values out of the object. I think it's simpler to put the stack accessors inline - sel_stack was written for adding a dynamic number of stacks. (YMMV)
.group(idStackGroup, 'Others', d => d.value.not2)
.stack(idStackGroup, 'Eeid of interest', d => d.value.is2);
https://jsfiddle.net/gordonwoodhull/fu4w96Lh/23/
(1) If you do any arithmetic on undefined it casts to NaN and NaN ruins all further calculations.
I am trying to get a layout that would, for each object in the data array:
append a rect or a g element that will serve as container
inside or on top of this, append a circle for each of the coordinates.
Below is a mock-up of how the data is massaged before I'm trying to append to the DOM (at the top of the update() function in the block below):
[{
label: 'foo',
circles: [
{ x: 0, y: 10 },
{ x: 10, y: 10 }
]
},{
...
}]
The drawing and updating of the rect elements seems to be working fine, but I am getting the selection and joins confused for the circles.
Here's the block: http://blockbuilder.org/basilesimon/91f75ab5209a62981f11d30a81f618b5
With
var dots = rects.selectAll('.dots')
I can select the right data below but can't draw it.
Could you help me getting the selections right so I can draw and update both the rect and the circle, please?
Thank you Gerard for your help. This is my current state, but I've pitted myself into a hole by running a for loop instead of d3 selections.
I wonder if I couldn't nest the circles in g elements after building a new data object like so:
var data = dataset.map(function(d) {
var circles = d3.range(d.amount).map(function(i) {
return {
x: (i % 5)*20,
y: (i / 5 >> 0)*20
}
});
return {
label: d.label,
dots: circles
};
});
From each object in data, we'll append a g, and inside each g we'll append the circles. Any help appreciated, since this will affect the dots + i used by the update pattern...
New question here
Here is the problem:
var dots = svg.selectAll('dots')
You're selecting something that doesn't exist. Because of that, your "enter" selection will always contain all the data, and your "exit" selection will always be empty.
The solution is changing it for something like this:
var dots = svg.selectAll(".dots" + i)
And, in the enter selection, setting the classes:
.attr("class", "dots" + i)
Here is your updated bl.ocks (with some other minor changes): https://bl.ocks.org/anonymous/4c2e1d66f1ab890da983465a4f84ca9b
I am trying to get the data in all the matching element using d3. I have following code
d3.selectAll('svg').selectAll('.line').data()
what i expect is that it should return data in all the matching element. but it just return data in first matching element.
if i just do
d3.selectAll('svg').selectAll('.line')
this shows that it has 2 group element and its data property contains the data.
if i just do var line = d3.selectAll('svg').selectAll('.line'); line[0].data()it gives me error. as line[0] become a DOM element without any property
how to get data in all matching selection or am i not clear on how to use it.
This is the expected behaviour as the spec on selection.data(values) reads:
If values is not specified, then this method returns the array of data
for the first group in the selection.
That explains why you only get the data bound to the first group.
To access data bound to all groups returned by your selection you could use:
d3.selectAll('svg').selectAll('.line').each(function(d) {
// Within this function d is this group's data.
// Iterate, accumulate, do whatever you like at this point.
});
I can not see your code, therefore I will show you a working one:
// scene setup
// we generate 3 SVG tags inside the page <body>
var $svg;
for (var svg = 0; svg < 3; svg++) {
$svg = d3.select("body").append("svg:svg").attr({
width: 200,
height: 200,
id: "svg_" + svg
});
}
// multiple selection + data
// consider the array of colors as data
var array_of_colors = ["red", "yellow", "blue", "khaki", "gray", "green"];
d3.selectAll("svg").selectAll("line.dash").data(array_of_colors).enter()
.append("svg:line").attr({
x1: function(d){return(50 + Math.random() * 50);},// random coordinates
y1: function(d){return(50 + Math.random() * 50);},// random coordinates
x2: 150,
y2: 140,
"stroke-width": 2,
stroke: function(d) {
console.log(d);
return (d);
}
}).classed({
"dash": true
});
The code produces 6 lines (as size of data array) in each SVG element:
Looks like:
I've been playing with the newly released version 2.0.0 and I'm trying to replicate the composite axis example with some simple opinion poll data - 3 series (Yes,No,Unsure) against a single Y-axis.
I'm clearly making a basic error somewhere... the basic principle is you define one y-axis then pass that as a reference each time you want to layer on another set of data, but it doesn't appear to be treating them as three distinct series (there are no lines between points except in cases linking a couple of identical, but non-adjacent values, e.g. "No" 30% in 2002 and 2004, despite there being a different value in between).
Also, as per the commented-out line, I'm getting a D3 error if I try and switch the x-axis to Time instead.
JSFiddle live example
pollData = [
{ Year : 2001, Yes: 50, No: 40, Unsure: 10 },
{ Year : 2002, Yes: 60, No: 30, Unsure: 10 },
{ Year : 2003, Yes: 65, No: 25, Unsure: 5 },
{ Year : 2004, Yes: 75, No: 30, Unsure: 4 },
{ Year : 2005, Yes: 80, No: 10, Unsure: 5 }
];
var svg = dimple.newSvg("#chartContainer", 590, 400);
var myChart = new dimple.chart(svg, pollData);
myChart.setBounds(75, 30, 490, 330)
// Why won't a time axis working...? ("undefined is not a function" in D3)
// var dateAxis = myChart.addTimeAxis("x", "Year", "%Y");
var dateAxis = myChart.addCategoryAxis("x", "Year");
dateAxis.addOrderRule("Year");
var yesAxis = myChart.addMeasureAxis("y", "Yes");
var noAxis = myChart.addMeasureAxis(yesAxis, "No");
var unsureAxis = myChart.addMeasureAxis(yesAxis, "Unsure");
myChart.addSeries("Yes", dimple.plot.line, [dateAxis, yesAxis]);
myChart.addSeries("No", dimple.plot.line, [dateAxis, noAxis]);
myChart.addSeries("Unsure", dimple.plot.line, [dateAxis, unsureAxis]);
yesAxis.title = '%';
myChart.draw();
The issue here isn't the composite axes, which you are using correctly. It's the series definition. The first parameter of the addSeries either takes dimensions to disaggregate by or a field which is not in the data, which it uses as a label. The only limitation of this approach is that you cannot label your series by a dimension name in the data. In this case the first series for example (it applies to all), is disaggregated by "Yes" which for the line series means it tries to draw a line for each value of "Yes", meaning 1 line per row, hence all the single unconnected points. The fix is to just name your lines something different. For example:
myChart.addSeries("Y", dimple.plot.line, [dateAxis, yesAxis]);
myChart.addSeries("N", dimple.plot.line, [dateAxis, noAxis]);
myChart.addSeries("U", dimple.plot.line, [dateAxis, unsureAxis]);
Here's your updated fiddle: http://jsfiddle.net/RawyW/2/
If you want the names to look like they match you can just add a trailing space to the series names:
myChart.addSeries("Yes ", dimple.plot.line, [dateAxis, yesAxis]);
myChart.addSeries("No ", dimple.plot.line, [dateAxis, noAxis]);
myChart.addSeries("Unsure ", dimple.plot.line, [dateAxis, unsureAxis]);
This will also work