How to create a tooltip with custom value - d3.js

i use NVD3 for a scatter chart but when hovering for the tooltip i want the label instead of the key.
this is my json:
long_data = [
{
key: 'PC1',
color: '#00cc00',
values: [
{
"label" : "Lichtpuntje" ,
"x" : 11.16,
"y" : -0.99,
"size":1000,
"color": '#FFCCOO'
} ,
{ ....
this is the js
nv.addGraph(function() {
chart = nv.models.scatterChart()
.showDistX(true)
.showDistY(true)
.useVoronoi(true)
.color(d3.scale.category10().range())
.transitionDuration(300)
...
chart.xAxis.tickFormat(d3.format('.02f'));
chart.yAxis.tickFormat(d3.format('.02f'));
chart.tooltipContent(function(i) { return labelArray[i]; });
d3.select('#test1 svg')
.datum(long_data)
.call(chart);
...
how i can i get the tooltip to have the label value? or how can i have i as index parameter?

ok, not a clean solution, but works:
chart.tooltipContent(function(key, y, e, graph) {
labelIndex=arrayContains(resultYVal, e);
return resultLabel[labelIndex];
});
function arrayContains(a, obj) {
var i = a.length;
while (i--) {
if (a[i] == obj) {
return i;
}
}
return false;
}

You can access your label variable like this:
chart.tooltipContent(function(graph) {
console.log(graph); //examine the graph object in the console for more info
return graph.point.label;
});

Newer NVD3 versions use tooltipGenerator. Also I don't understand why shev72 is iterator over the whole series, we're getting the index in the series by NVD3 directly, e.g. if our data looks like this:
data = [{"key": "Group 0", "values": [{"y": 1.65166973680992, "x": 0.693722035658137, "z": "SRR1974855"}, {"y": 1.39376073765462, "x": -0.54475764264808, "z": "SRR1974854"}]
then you can get your z values like this:
chart.tooltip.contentGenerator(function (d) {
var ptIdx = d.pointIndex;
var serIdx = d.seriesIndex;
z = d.series[serIdx].values[ptIdx].z
return z;
});

For anyone here having issues trying to show a custom tooltip with InteractiveGuideline enabled, you must use the following:
chart.interactiveLayer.tooltip.contentGenerator(function (obj) { ... });
As a bonus, here's the code for the default table layout with an added custom value 'z':
chart.interactiveLayer.tooltip.contentGenerator(function (obj) {
var date = obj.value; // Convert timestamp to String
var thead = '<thead><tr><td colspan="4"><strong class="x-value">'+ date +'</strong></td></tr></thead>';
var table = '<table>' + thead + '<tbody>';
// Iterate through all chart series data points
obj.series.forEach(dataPoint => {
// Get all relevant data
var color = dataPoint.color;
var key = dataPoint.key;
var value = dataPoint.value;
var string = dataPoint.data.z; // Custom value in data
var row = '<tr><td class="legend-color-guide"><div style="background-color: '+ color +';"></div></td><td class="key">'+ key +'</td><td class="string">'+ string +'</td><td class="value">'+ value +'</td></tr>';
table += row;
});
// Close table & body elements
table += '</tbody></table>';
return table;
});

I found the answer from davedriesmans quite useful, however note that in the code
chart.tooltipContent(function(key, y, e, graph)) is not for a scatterPlot.
This looks like the function for a pie chart. In that case you can use the e.pointIndex directly to allow you to index into the series by graph.series.values[e.pointIndex].
However, I built on the function davedriesmans suggested for a scatterplot as follows.
function getGraphtPt(graph, x1, y1) {
var a = graph.series.values;
var i = a.length;
while (i--) {
if (a[i].x==x1 & a[i].y==y1) {
return a[i];
}
}
return null;
}
the main chart call to insert the tooltip is just
chart.tooltipContent(function (key, x, y, graph) {
s = "unknown";
pt = getGraphtPt(graph, x, y);
if (pt != null) {
//below key1 is a custom string I added to each point, this could be 'x='+pt.x, or any custom string
// I found adding a custom string field to each pt to be quite handy for tooltips
s = pt.key1;
}
return '<h3>' + key + s + '</h3>';
});

Related

Time Series chart to filter multi line chart(rendering multi lines for multiple data items)

What is the right way to filter multi line chart using a time series chart as filter?
I need a time series chart for my focus chart that it is shown the image below. Whenever I brush on time series chart my focus chart needs to be filtered with respect to time series chart.
The time series chart needs to contain only X axis and time as its dimension and it should be interactive with focus chart with respect to time.
var totalNumber = null;
// ------ main chart function -------
function makeCompetitiveGraphs(error, keywords_data) {
errorHandle(error);
cleanedData = getCompositeChartData(keywords_data);
console.log("===", cleanedData);
minDate = moment.min(cleanedData.timeStamp);
maxDate = moment.max(cleanedData.timeStamp);
margins = { top: 27, right: 27, bottom: 36, left: 54 };
// create composite chart.
var composite = dc.compositeChart('#competitiveChart');
// create cross filter
var cf = crossfilter(cleanedData.keywordData);
// create dimensions.
var keywordDateDimension = cf.dimension(function (dp) { return dp.date;
});
var Group = keywordDateDimension.group();
// compose for key words
composeCharts = composeKeywords(dc, composite, keywordDateDimension);
// create chart.
composite
.width(width())
.height(height())
.transitionDuration(1000)
.x(d3.time.scale().domain([minDate, maxDate]))
.ordering(function (d) { return d.value; })
.elasticY(true)
.elasticX(true)
.margins(margins)
.legend(
dc.legend()
.x(1100)
.y(10)
.itemHeight(16)
.gap(8)
.horizontal(false)
)
.renderHorizontalGridLines(true)
.brushOn(false);
// compose the chart array.
composite.compose(composeCharts);
// render the chart
composite.render();
function getCompositeChartData(keywords) {
let momentTimeStamps = [];
let totalKeywordPerDay = [];
let allKeywords = [];
// clean data for d3js chart's
keywords.forEach((kob) => {
kob.sd.forEach((ob) => {
allKeywords.push({
name: kob.kn,
total: ob.value.total__,
date: new Date(moment(ob._id.mention_created_date_, "MMM-DD-YYYY-hh")._d),
});
momentTimeStamps.push(moment(moment(ob._id.mention_created_date_, "MMM-DD-
YYYY-hh")._d));
totalKeywordPerDay.push(ob.value.total__);
});
});
console.log("--------", allKeywords)
// apply date filter.
allKeywords = limiteDataToDateFilter(allKeywords);
return { "keywordData": allKeywords, "timeStamp": momentTimeStamps,
"totalKeywordPerDay": totalKeywordPerDay };
}
function limiteDataToDateFilter(allKeywords) {
cleanedDateWithDates = [];
allKeywords.forEach(element => {
if (moment(element.date).isAfter(moment().date(1).month(6)) &&
moment(element.date).isBefore(moment().date(30).month(8))) {
cleanedDateWithDates.push(element);
}
});
return cleanedDateWithDates;
}
function getReduce(keyword, keywordDateDimension) {
return keywordDateDimension.group().reduceSum(function (dp) {
return dp.name === keyword ? dp.total : 0;
});
}
function composeKeywords(dc, composite, keywordDateDimension) {
composeChartsData = []
keywordsParams.forEach(keyword => {
keyword.chart = dc.lineChart(composite)
.dimension(keywordDateDimension)
.colors(keyword.color)
.group(getReduce(keyword.word, keywordDateDimension), keyword.word)
.interpolate('basis')
composeChartsData.push(keyword.chart);
});
return composeChartsData;
}

dc.js HeatMap crossfilter grey un-selected

I would like my heatmap to react the same way if the date on the heatmap's x asis is selected or if the date is selected from the yearSlicer (rowChart)
I've tried using these posts:
Is there a way to set a default color for the un-selected boxes in heatmap in dc.js when crossfilter is applied?
dc.js heatmap deselected colors
.colors function in barChart dc.js with only two options
Trying to Assign Color for Null values in a map D3,js v4
yearSlicer(rowChart):
var yearSlicerDimension = ndx.dimension(function (d) {
return "" + d.Year;
})
yearSlicerGroup = yearSlicerDimension.group().reduceSum(function (d) {
return d.FTE;
});
yearSlicer
.title("")
.width(300)
.height(480)
.dimension(yearSlicerDimension)
.group(yearSlicerGroup)
.valueAccessor(function (kv) {
return kv.value > 0 ? 1 : 0; // If funtion to detect 0's
})
.ordering(function (d) { return -d.Year })
.xAxis().ticks(0)
Heat Map:
var dimension = ndx.dimension(function (d) { return [d.Year, d["Manager"]]; }),
FTEMonthGroup = dimension.group().reduce(
function reduceAdd(p, v) {
if (p.n === 0) {
p.color = 0;
p.toolTip = 0;
}
++p.n;
p.color += v.FTE;
p.toolTip += v.FTE;
return p;
},
function reduceRemove(p, v) {
--p.n;
p.color = "null";
return p;
},
function reduceInitial() {
return { n: 0, color: 0, toolTip: 0 };
});
heatMap
.width(900)
.height(800)
.dimension(dimension)
.group(FTEMonthGroup)
.margins({ left: 200, top: 30, right: 10, bottom: 20 })
.keyAccessor(function (d) { return d.key[0]; })
.valueAccessor(function (d) { return +d.key[1]; })
.colorAccessor(function (d) { return +d.value.color; })
.title(function (d) {
return "Manager: " + d.key[1] + "\n" +
"FTE: " + d.value.toolTip + "\n" +
"Date: " + d.key[0] + "";
})
.calculateColorDomain()
.on('renderlet', function (chart) {
chart.selectAll("g.cols.axis text")
.attr("transform", function () {
var coord = this.getBBox();
var x = coord.x + (coord.width / 2),
y = coord.y + (coord.height / 2);
return "rotate(-45 " + x + " " + y + ")"
})
});
What I would like:
What I get:
Right now it looks like your color accessor will return NaN for any bin which has anything removed from it, because
+"null" === NaN
Note that
+null === 0
but we don't necessarily want that, because we want an exceptional color for bins that had everything removed, not "the zero color".
It would be easier to try it out if you included a fiddle or something, but I think you should be able to get there with the following changes:
Properly null out your color when n goes to zero:
function reduceAdd(p, v) {
++p.n;
p.color += v.FTE;
p.toolTip += v.FTE;
return p;
},
function reduceRemove(p, v) {
--p.n;
if (p.n === 0) {
p.color = null;
p.toolTip = null;
}
return p;
},
We only need to check on remove, not on add, because n never goes to zero on add, by definition. It's okay to pair = null with += v.FTE because if you use += on a variable that contains null, it will get automatically coerced to 0 (unlike undefined which will go to NaN).
Use a color calculator to detect null and produce gray
Use the recently un-deprecated colorCalculator to detect nulls and use the gray of our choosing for them. Otherwise we pass the color through the color scale as usual:
heatMap
.colorCalculator(function(d, i) {
return d.value.color === null ?
'#ccc' : heatMap.colors()(d.value.color);
});
Note that internally, dc.js charts use the .selected and .deselected classes to show which items are selected by the brush and which are not, but that would be complicated here (especially if you still want the brush to work), so it's easier just to use the same color.
Again, this is all untested, but I think it's the right principle. Lmk if there are any issues and I'll be glad to fix it.
alternately... scale.unknown()?
It may also be possible to use scale.unknown() for this purpose, but it is brand-new to D3 5.8 and I haven't tried it. Something like
heatMap.colors().unknown('#ccc')

dc.js bubble chart - multidimension grouping issue and unable to get custom reducer to work

I'm currently trying to produce a dashboard in dc.js for my master's thesis and I have hit a real roadblock today if anyone could please help it would be much appreciated. I'm new to Javascript and dc so I'll try my best to explain...
My data format (Probe Request with visible SSID):
{"vendor":"Huawei Technologies Co.Ltd","SSID":"eduroam","timestamp":"2018-07-10 12:25:26","longitude":-1.9361,"mac":"dc:d9:16:##:##:##","packet":"PR-REQ","latitude":52.4505,"identifier":"Client"}
My data format (Probe Request with Broadcast / protected SSID):
{"vendor":"Nokia","SSID":"Broadcast","timestamp":"2018-07-10 12:25:26","longitude":-1.9361,"mac":"dc:d9:16:##:##:##","packet":"PR-REQ","latitude":52.4505,"identifier":"Client"}
I'm trying to produce a bubble chart which will display vendors as a bubble (size denoted by volume of packets collected for that vendor) then plot the bubble against X axis unprotected (any SSID != broadcast) & Y axis protected (packets where "Broadcast" is in the data)
Sketch of what I mean
What I've managed to get so far
I've managed to get a bar/ row/pie charts to work as they only require me to use one dimension and run them through a group. But I think I'm fundamentally misunderstanding how to pass multiple dimensions to a group.
for each at the top adds a new value of 0 / 1 to triple if Broadcast is present in the data.
Then I'm using a custom reducer to count 0 / 1 related to unpro & pro which will be used to plot the X / Y
I've tried reworking the nasdaq example and I'm getting nowhere
Code:
queue()
.defer(d3.json, "/uniquedevices")
.await(plotVendor);
function plotVendor(error, packetsJson) {
var packets = packetsJson;
packets.forEach(function (d) {
if(d["SSID"] == "Broadcast") {
d.unpro = 0;
d.pro = 1;
} else {
d.unpro = 1;
d.pro = 0;
}
});
var ndx = crossfilter(packets);
var vendorDimension = ndx.dimension(function(d) {
return [ d.vendor, d.unpro, d.pro ];
});
var vendorGroup = vendorDimension.group().reduce(
function (p, v) {
++p.count;
p.numun += v.unpro;
p.numpr += v.pro;
return p;
},
function (p, v) {
--p.count;
p.numun -= v.unpro;
p.numpr -= v.pro;
return p;
},
function () {
return {
numun: 0,
numpr: 0
};
}
);
var vendorBubble = dc.bubbleChart("#vendorBubble");
vendorBubble
.width(990)
.height(250)
.transitionDuration(1500)
.margins({top: 10, right: 50, bottom: 30, left: 40})
.dimension(vendorDimension)
.group(vendorGroup)
.yAxisPadding(100)
.xAxisPadding(500)
.keyAccessor(function (p) {
return p.key[1];
})
.valueAccessor(function (p) {
return p.key[2];
})
.radiusValueAccessor(function (d) { return Object.keys(d).length;
})
.maxBubbleRelativeSize(0.3)
.x(d3.scale.linear().domain([0, 10]))
.y(d3.scale.linear().domain([0, 10]))
.r(d3.scale.linear().domain([0, 20]))
dc.renderAll();
};
Here is a fiddle: http://jsfiddle.net/adamistheanswer/tm9fzc4r/1/
I think you are aggregating the data right and the missing bits are
your accessors should look inside of value (that's where crossfilter aggregates)
.keyAccessor(function (p) {
return p.value.numpr;
})
.valueAccessor(function (p) {
return p.value.numun;
})
.radiusValueAccessor(function (d) {
return d.value.count;
})
your key should just be the vendor - crossfilter dimensions aren't geometric dimensions, they are what you filter and bin on:
var vendorDimension = ndx.dimension(function(d) {
return d.vendor;
});
you probably need to initialize count because ++undefined is NaN:
function () { // reduce-init
return {
count: 0,
numun: 0,
numpr: 0
};
}
Fork of your fiddle, with all the dependencies added, wrapping function disabled, and elasticX/elasticY (probably not what you want but easier to debug):
https://jsfiddle.net/gordonwoodhull/spw5oxkj/16/

How to decide dimensions and groups in dc.js?

I am new to dc.js and facing issues in deciding dimensions and groups. I have data like this
this.data = [
{Type:'Type1', Day:1, Count: 20},
{Type:'Type2', Day:1, Count: 10},
{Type:'Type1', Day:2, Count: 30},
{Type:'Type2', Day:2, Count: 10}
]
I have to show a composite chart of two linecharts one for type Type1 and other for Type2. My x-axis will be Day. So one of my dimensions will be Day
var ndx = crossfilter(this.data);
var dayDim = ndx.dimension(function(d) { return d.Day; })
How the grouping will be done? If I do it on Count, the total count of a particular Day shows up which I don't want.
Your question isn't entirely clear, but it sounds like you want to group by both Type and Day
One way to do it is to use composite keys:
var typeDayDimension = ndx.dimension(function(d) {return [d.Type, d.Day]; }),
typeDayGroup = typeDayDimension.group().reduceSum(function(d) { return d.Count; });
Then you could use the series chart to generate two line charts inside a composite chart.
var chart = dc.seriesChart("#test");
chart
.width(768)
.height(480)
.chart(function(c) { return dc.lineChart(c); })
// ...
.dimension(typeDayDimension)
.group(typeDayGroup)
.seriesAccessor(function(d) {return d.key[0];})
.keyAccessor(function(d) {return +d.key[1];}) // convert to number
// ...
See the series chart example for more details.
Although what Gordon suggested is working perfectly fine, if you want to achieve the same result using composite chart then you can use group.reduce(add, remove, initial) method.
function reduceAdd(p, v) {
if (v.Type === "Type1") {
p.docCount += v.Count;
}
return p;
}
function reduceRemove(p, v) {
if (v.Type === "Type1") {
p.docCount -= v.Count;
}
return p;
}
function reduceInitial() {
return { docCount: 0 };
}
Here's an example: http://jsfiddle.net/curtisp/7frw79q6
Quoting Gordon:
Series chart is just a composite chart with the automatic splitting of the data and generation of the child charts.

Update multiple tags rowchart in dc.js

I am looking for how to create a rowchart in dc.js to show and filter items with multiple tags. I've summed up a few answers given on stack overflow, and now have a working code.
var data = [
{id:1, tags: [1,2,3]},
{id:2, tags: [3]},
{id:3, tags: [1]},
{id:4, tags: [2,3]},
{id:5, tags: [3]},
{id:6, tags: [1,2,3]},
{id:7, tags: [1,2]}];
var content=crossfilter(data);
var idDimension = content.dimension(function (d) { return d.id; });
var grid = dc.dataTable("#idgrid");
grid
.dimension(idDimension)
.group(function(d){ return "ITEMS" })
.columns([
function(d){return d.id+" : "; },
function(d){return d.tags;},
])
function reduceAdd(p, v) {
v.tags.forEach (function(val, idx) {
p[val] = (p[val] || 0) + 1; //increment counts
});
return p;
}
function reduceRemove(p, v) {
v.tags.forEach (function(val, idx) {
p[val] = (p[val] || 0) - 1; //decrement counts
});
return p;
}
function reduceInitial() {
return {};
}
var tags = content.dimension(function (d) { return d.tags });
var groupall = tags.groupAll();
var tagsGroup = groupall.reduce(reduceAdd, reduceRemove, reduceInitial).value();
tagsGroup.all = function() {
var newObject = [];
for (var key in this) {
if (this.hasOwnProperty(key) && key != "") {
newObject.push({
key: key,
value: this[key]
});
}
}
return newObject;
}
var tagsChart = dc.rowChart("#idtags")
tagsChart
.width(400)
.height(200)
.renderLabel(true)
.labelOffsetY(10)
.gap(2)
.group(tagsGroup)
.dimension(tags)
.elasticX(true)
.transitionDuration(1000)
.colors(d3.scale.category10())
.label(function (d) { return d.key })
.filterHandler (function (dimension, filters) {
var fm = filters.map(Number)
dimension.filter(null);
if (fm.length === 0)
dimension.filter(null);
else
dimension.filterFunction(function (d) {
for (var i=0; i < fm.length; i++) {
if (d.indexOf(fm[i]) <0) return false;
}
return true;
});
return filters;
}
)
.xAxis().ticks(5);
It can be seen on http://jsfiddle.net/ewm76uru/24/
Nevertheless, the rowchart is not updated when I filter by one tag. For example, on jsfiddle, if you select tag '1', it filters items 1,3,6 and 7. Fine. But the rowchart is not updated... I Should have tag '3' count lowered to 2 for example.
Is there a way to have the rowchart tags counts updated each time I filter by tags ?
Thanks.
After a long struggle, I think I have finally gathered a working solution.
As said on crossfilter documentation : "a grouping intersects the crossfilter's current filters, except for the associated dimension's filter"
So, the tags dimension is not filtered when tag selection is modified, and there is no flag or function to force this reset. Nevertheless, there is a workaround (given here : https://github.com/square/crossfilter/issues/146).
The idea is to duplicate the 'tags' dimension, and to use it as the filtered dimension :
var tags = content.dimension(function (d) { return d.tags });
// duplicate the dimension
var tags2 = content.dimension(function (d) { return d.tags });
var groupall = tags.groupAll();
...
tagsChart
.group(tagsGroup)
.dimension(tags2) // and use this duplicated dimension
as it can been seen here :
http://jsfiddle.net/ewm76uru/30/
I hope this will help.

Resources