amCharts 4 - How to Access Currently-Hovered Series Data/Color in XYChart in JavaScript - amcharts

I'm trying to access the currently-hovered series data and color via JavaScript. The data is available to the legend and tooltip, but I'm not sure how to directly access it.
It's possible to place the legend in an external container, but their code creates a lot of additional containers/wrappers which makes formatting difficult. This Github question addresses it, but no answer was provided.
Perhaps events could be used to detect changes in the legend text or tspan elements and then grab the new text, but I'm not sure how to do this (using amCharts events) and how efficient it would be (especially with multiple series and/or charts with synced cursors).
Another idea was to get the data based on cursor position, but this seems inefficient (cursorpositionchanged fires too often - on mouse/cursor movement even when the series data hasn't changed). Maybe it could be done more efficiently based on change in dateAxis value? For example, using the positionchanged event listener:
chart.cursor.lineX.events.on('positionchanged', function() {
// get series data and do something with it
});
At least when using chart.cursor.xAxis = dateAxis, the positionchanged event only seems to fire when the cursor jumps to a new value. So it would be more efficient than an event that fired on mouse/cursor movement.
Any suggestions would be appreciated.
UPDATE
By currently-hovered, I am referring to the series data and color accessible via the tooltip (for example) with the mouse over the chart.
Examples: CandlestickSeries and LineSeries

One method you can try is to set an adapter for tooltipText on the object of concern. Since this may run multiple times especially via a chart cursor, perhaps keep track of changes to the tooltip via monitoring the unique value, e.g. in the samples provided that would be the date field. The data you're looking for can be found in the adapter's target.tooltipDataItem. The color, if on the series, will be target.tooltipDataItem.component.fill (in the case of the line series example, the target is the line series and has no change of color, so you can just use target.fill), otherwise e.g. in the case of CandleStick series the color would be on the candle stick, or column, i.e. via target.tooltipDataItem.column.fill.
Sample adapter for LineSeries:
var tooltipDate;
series.adapter.add("tooltipText", function(text, target) {
// data via target.tooltipDataItem.dataContext
console.log('text adapter; color: ', target.tooltipDataItem.component.fill.hex);
if (tooltipDate !== target.tooltipDataItem.dataContext.date) {
console.log('new tooltip date, do something');
tooltipDate = target.tooltipDataItem.dataContext.date;
}
// note: in this case: component === target
return text;
});
Demo:
https://codepen.io/team/amcharts/pen/9f621f6a0e5d0441fe55b99a25094e2b
Sample Candlestick series adapter:
var tooltipDate;
series.adapter.add("tooltipText", function(text, target) {
// data via target.tooltipDataItem.dataContext
console.log('text adapter; color: ', target.tooltipDataItem.column.fill.hex);
if (tooltipDate !== target.tooltipDataItem.dataContext.date) {
console.log('new tooltip date, do something');
tooltipDate = target.tooltipDataItem.dataContext.date;
}
return text;
});
Demo:
https://codepen.io/team/amcharts/pen/80343b59241b72cf8246c266d70281a7
Let us know if this is making sense, and if the adapter route is a good point in time to capture changes, data, color, as well as if it's efficient enough a manner to go about this.

Related

Tooltip with Multi-Series Line Chart in Vega-Lite API

I am trying to recreate this example in Vega-Lite API in an Observable notebook. I am able to recreate the ruler with the multiple line series from another example in Observable. But I am having trouble adding tooltips, I would like to add the symbol ticker and the price of the stock. Here is my Observable notebook. Where would I put the tooltip specifications? Thanks!
plot = {
// select a point for which to provide details-on-demand
const hover = vl.selectSingle()
.encodings('x') // limit selection to x-axis value
.on('mouseover') // select on mouseover events
.nearest(true) // select data point nearest the cursor
.empty('none'); // empty selection includes no data points
// define our base line chart
const line = vl.markLine()
.data(stocks)
.encode(
vl.x().fieldT('date'),
vl.y().fieldQ('price'),
vl.color().fieldN('symbol'),
);
// shared base for new layers, filtered to hover selection
const base = line.transform(vl.filter(hover));
return vl.data(stocks)
.layer(
line,
// add a rule mark to serve as a guide line
vl.markRule({color:'#c0c0c0'})
.transform(
vl.filter(hover),
vl.pivot({pivot: 'symbol', value: 'price', groupby: ['date']}))
.encode(vl.x().fieldT('date'),
vl.tooltip().fieldQ('price')),
// add circle marks for selected time points, hide unselected points
line.markCircle()
.select(hover) // use as anchor points for selection
.encode(vl.opacity().if(hover, vl.value(1)).value(0),
vl.tooltip(['symbol','price']))
)
.render(); }
Here's how you use pivot on that example
vl.pivot('symbol').value('price').groupby( ['date']))
The pivot there helps you getting the data into table format, so you can have all the symbol prices available in one row. Here is a full working example of a Vega-Lite API multi-line series chart with tooltips:
https://observablehq.com/#vega/multi-series-line-chart-with-tooltip

How to apply brushing on a dynamically growing dataset?

I have a dynamically growing timeseries I need to display in a zoomable/panable chart.
Try it out here (in fact: my first jsFiddle ever :) ) :
https://jsfiddle.net/Herkules001/L12k5zwx/29/
I tried to do it the same way as described here: https://dc-js.github.io/dc.js/examples/replacing-data.html
However, each time the chart updates, the zoom and filter are lost on the focus chart. (The brush is preserved on the range chart however.)
How can I add data without resetting the views and losing the zoom?
var chart = dc.lineChart("#test");
var zoom = dc.lineChart("#zoom");
//d3.csv("morley.csv", function(error, experiments) {
var experiments = d3.csvParse(d3.select('pre#data').text());
experiments.forEach(function(x) {
x.Speed = +x.Speed;
});
var ndx = crossfilter(experiments),
runDimension = ndx.dimension(function(d) {return +d.Run;}),
speedSumGroup = runDimension.group().reduceSum(function(d) {return d.Speed * d.Run / 1000;});
chart
.width(768)
.height(400)
.x(d3.scaleLinear().domain([6,20]))
.brushOn(false)
.yAxisLabel("This is the Y Axis!")
.dimension(runDimension)
.group(speedSumGroup)
.rangeChart(zoom);
zoom
.width(768)
.height(80)
.x(d3.scaleLinear().domain([6,20]))
.brushOn(true)
.yAxisLabel("")
.dimension(runDimension)
.group(speedSumGroup);
zoom.render();
chart.render();
var run = 21;
setInterval(
() => {
var chartfilter = chart.filters();
var zoomfilter = zoom.filters();
chart.filter(null);
zoom.filter(null);
ndx.add([{Expt: 6, Run: run++, Speed: 100 + 5 * run}]);
chart.x(d3.scaleLinear().domain([6,run]));
zoom.x(d3.scaleLinear().domain([6,run]));
chart.filter([chartfilter]);
zoom.filter([zoomfilter]);
chart.render();
zoom.render();
},
1000);
//});
In this case, if you are just adding data, you don't need to do the complicated clearing and restoring of filters demonstrated in the example you cited.
That part is only necessary because crossfilter.remove() originally would remove the data that matched the current filters. An awkward interface, almost never what you want.
If you're only adding data, you don't have to worry about any of that:
setInterval(
() => {
ndx.add([{Expt: 6, Run: run++, Speed: 5000 + 5 * run}]);
chart.redraw();
zoom.redraw();
},
5000);
Note that you'll get less flicker, and decent animated transitions, by using redraw instead of render. I also added evadeDomainFilter to avoid lines being clipped before the edge of the chart.
Fork of your fiddle
Removing data
If you use the predicate form of crossfilter.remove() you don't have to worry about saving and restoring filters:
ndx.remove(d => d.Run < run-20);
However, this does expose other bugs in dc.js. Seems like elasticY does not work, similar to what's described in this issue. And you get some weird animations.
Here's a demo with remove enabled.
In the end, dc.js has some pretty neat features, and there is usually a way to get it to do what you want, but it sure is quirky. It's a very complicated domain and in my experience you are going to find some of these quirks in any fully featured charting library.
Update: I fixed the replacing data example, that one is just ndx.remove(() => true) now.
zooming issues
As Joerg pointed out in the comments,
when the chart is not zoomed, it would be nice to have it also grow to show new data as it arrives
the X domain was clipped or even reversed if the focus reached outside the original domain of the chart
We can address these issues by adding a preRedraw event handler. That's the ideal place to adjust the domain; for example you can implement elasticX manually if you need to. (As you'll see in a second, we do!)
First, a naive attempt that's easy to understand:
chart.on('preRedraw', () => {
chart.elasticX(!zoom.filters().length);
});
We can turn elasticX on and off based on whether the range chart has an active filter.
This works and it's nice and simple, but why does the chart get so confused when you try to focus on a domain that wasn't in the original chart?
Welp, it records the original domain (source). So that it can restore to that domain if the focus is cleared, and also to stop you from zooming or panning past the edge of the graph.
But notice from the source link above that we have an escape hatch. It records the original domain when the X scale is set. So, instead of setting elasticX, we can calculate the extent of the data, set the domain of the scale, and tell the chart that the scale is new:
chart.on('preRedraw', () => {
if(!zoom.filters().length) {
var xExtent = d3.extent(speedSumGroup.all(), kv => kv.key);
chart.x(chart.x().domain(xExtent));
}
});
New fiddle with zooming issues fixed.
There is still one glitch which Joerg points out: if you are moving the brush while data comes in, the brush handles occasionally will occasionally stray from the ends of the brush. In my experience, these kinds of glitches are pretty common in D3 (and dynamic charting in general), because it's difficult to think about data changing during user interaction. It probably could be fixed inside the library (perhaps an interrupted transition?) but I'm not going to get into that here.

snapping brush to the next x value? [dc.js]

I have a line chart and data in the form
[{
time: "2016-4-29"
total: 23242
},
{
time: "2016-5-16
total: 3322
}
...
]
I'm trying to filter on the x-axis with the brush, however, since I don't have every single date, if I brush in a small range, the filter handler seems to return an empty array for my filters
I've set up my line chart's x-axis like so:
.x(d3.time.scale().domain([minDate,maxDate]))
is there a way to make it so a user can only filter on dates that are in the dataset?
I would like the brush to snap to dates in the dataset.
it seems like whats happening is that you are able to brush between ticks..so it doesn't know what it selected.
I'm going to answer the easier question: How do I create a brush that will not allow nothing to be selected?
In other words, if the brush contains no data, do not allow it to take.
There are two parts to the solution. First, since any chart with a brush will remove the old filter and then add the new filter, we can set up the addFilterHandler to reject any filter that does not contain non-zero bins:
spendHistChart.addFilterHandler(function(filters, filter) {
var binsIn = spendHistChart.group().all().filter(function(kv) {
return filter.isFiltered(kv.key) && kv.value;
});
console.log('non-empty bins in range', binsIn.length);
return binsIn.length ? [filter] : [];
});
That's the straightforward part, and incidentally I think you could probably modify it to snap the brush to existing data. (I haven't tried it, though.)
The more tricky part is that this won't get rid of the brush, it just doesn't apply the filter. So the chart will end up in an inconsistent state.
We need to detect when the brush action has finished, and if there is no filter at that point, explicitly tell the chart to clear the filter:
spendHistChart.brush().on('brushend.no-empty', function() {
if(!spendHistChart.filters().length)
window.setTimeout(function() {
spendHistChart.filterAll().redraw();
}, 100);
});
We need a brief delay here, because if we respond to brushend synchronously, the chart may still be responding to it, causing bickering and dissatisfaction.
As a bonus, you get kind of a "nah-ah" animation because of the unintentional remove-brush animation.
demo fiddle

dc.js Weird mouse zooming for seriesChart

I want to use the mouse zoom functionality on seriesChart and have it filter for other charts of the same group.
When I enable the zoom with .mouseZoomable(true) on seriesChart, and zoom the chart, the other charts become empty.
This doesn't happen when I enable it on a LineChart.
Here is a simple example: https://codepen.io/udeste/pen/ZKeXmX
(Zoom the second chart with the mouse. All is working. But when you zoom the first chart the other charts go blank.)
What am I doing wrong? Is it a dc.seriesChart bug?
It's because dc.seriesChart required you to supply that strange dimension, but it didn't change the filter function accordingly.
You specified seriesDimension like so:
var seriesDimension = ndx.dimension(function(d) {
return [+d.Expt, +d.Hours];
});
But when you zoom, the dc.coordinateGridMixin just filters using a regular dc.filters.RangedFilter, which does not know about these kinds of two-dimensional "multikeys".
Probably since the series chart requires this kind of input, it should redefine the filter handler to also deal with multikeys. But until then, you can work around it by providing your own filterHandler:
seriesChart.filterHandler(function(dimension, filters) {
if(filters.length === 0) // 1
dimension.filter(null);
else {
console.assert(filters.length===1); // 2
console.assert(filters[0].filterType==='RangedFilter');
dimension.filter(function(d) { // 3
return filters[0][0] <= d[1] && d[1] < filters[0][1];
})
}
});
What this does:
Checks if this event is because the filters have been cleared, and clears the dimension's filter if so.
Asserts that the filter is what is expected. coordinateGridMixin will always supply a single dc.filters.RangedFilter but who knows what else could happen.
Supplies a substitute filter function that checks if the part of the key used by the keyAccessor falls within the range (instead of comparing the array with the range, which will always return false).
Here's a working fork of your codepen.
(Incidentally, it looks like this examples slams into a known issue where line segments off the edge of the chart are dropped instead of clipping the segments. It won't be quite as bad if there are more points. I don't think I've found a good workaround. Hopefully we'll fix this soon.)

d3.js tween factory return function applied to non-interpolable property values

This question builds on the (correct) answer provided to this. I simply haven't been able to get any further..
With the help of an interpolator function, d3.js's tween allows smooth graphical transition between existing and new (ie to be set) DOM element values. At the simplest level, for a given animation we have a target element, an start state, an end state, a transition, a tween function and an interpolator.
Now, say I want every so often to programmatically update the contents of an input (text field) element. The value to be entered is non-interpolable (either the text is submitted, or it is not. There is no in-between state). In providing a closure (allowing for text retrieval at the scheduled transition time), tween would seem to be a good vehicle for the updates. Either I replace the interpolator with a fixed value, ensure the start and end values are identical, or find some other way of forcing it to fire at t=1. That's the theory..
To this end, in my case each property (not value) is modified in it's own update call, into which are passed transition, element index and parent element selection.
First cut:
an outer, 'governing' transition with delay values staggered using a multiple of the current element's index
playback_transition = d3.transition()
.delay(function(d, i, j) {
return (time_interval * i);
})
.duration(function() {
return 1; // the minimum
});
within a call to playback_transition.each() pass the transition as a parameter to a dependent animation by means of an update() interface
within this dependent animation, apply the transition and tween to the current element(s):
input // a UI dialog element
.transition()
.tween(i.toString(), setChordname( waveplot.chordname ));
Where:
function setChordname(newValue) {
return function() {
var i = newValue; // a string
return function(t) {
this.value = i;
inputChanged.call(this);
};
};
};
and
function inputChanged() {
if (!this.value) return;
try {
var chord = chordify.chordObjFromChordName(this.value);
purge(); // rid display of superceded elements
plotChord(chord, options); // calculate & draw chord using new input property
} catch (e) {
console.log(e.toString());
}
}
PROBLEM
While setChordname always fires (each chord is in turn correctly found and it's value stored), of the scheduled returned functions, only the first fires and results in display of the associated waveform. For all subsequent return function occurrences, it is as if they had never been scheduled.
From the display:
direct user update to the input field still works fine
only the first of setChordname's return functions fire, but, for this initial chord, carries right through, correctly displaying the cluster of associated chord and note waves.
From this, we can say that the problem has nothing to do with the integrity of the waveplotting functions.
From the console
transitions are accumulating correctly.
chord supply is all good
associated (ie initial) tween fires at t=1. (specifically, tween appears to accept omission of an interpolator function).
looking at the output of transition.toSource(), though the associated outer index increases by single figure leaps, tween itself is always paired with an empty pair of curly brackets.
transition = [[{__transition__:{8:{tween:{}, time:1407355314749, eas..
For the moment, apart from this and the initial execution, the tween factory return function behaviour is a mystery.
From Experiment
Neither of the following have any impact:
Extending the period before the initial transition takes effect
Extending (by a multiple) each staggered transition delay
Furthermore
the same transition configuration used in a different scenario works fine.
These seem to eliminate timing issues as a possible cause, leaving the focus more on the integrity of the tween setup, or conditions surrounding waveplot append/remove.
Afraid it might be interfering with input property text submission via the tween, I also tried disabling a parallel event listener (listening for 'change' events, triggering a call to inputChanged()). Apart from no longer being able to enter own chordnames by hand, no impact.
In addition to 'change', I tried out a variety of event.types ('submit', 'input', 'click' etc). No improvement.
The single most important clue is (to my mind) that only the first setChordname() return function is executed. This suggests that some fundamental rule of tween usage is being breached. The most likely candidate seems to be that the return value of tween **must* be an interpolator.
3 related questions, glad of answers to any:
Anything blatently wrong in this approach?
For a shared transition scenario such as this, do you see a better approach to transitioning a non-interpolable (and normally user-supplied) input property than using tween ?
Provided they are staggered in time, multiple transitions may be scheduled on the same element - but what about multiple tweens? Here, as the staggered transition/tween combos are operating on only one element, they seem likely to be passed identical data (d) and index(i) in every call. Impact?
I'm now able to answer my own question. Don't be put off by the initial couple of paragraphs: there are a couple of valuable lessons further down..
Ok, there were one or two trivial DOM-to-d3 reworking issues in my adoption of the original code. Moreover, an extra returned function construct managed to find it's way into this:
Was:
function setChordname(newValue) {
return function() { <--- Nasty..
var i = newValue;
return function(t) {
this.value = i;
inputChanged.call(this);
};
};
};
Should have been:
function setChordname(newValue) {
var i = newValue;
return function(t) {
this.value = i;
inputChanged.call(this);
};
};
The fundamental problem, however, was that the transition -passed in as a parameter to an update() function- seems in this case to have been blocked or ignored.
Originally (as documented in the question) defined as:
input // a UI dialog element
.transition()
.tween(i.toString(), setChordname( waveplot.chordname ));
..but should have been defined as:
transition
.select("#chordinput") // id attribute of the input element
.tween(i.toString(), setChordname( waveplot.chordname ));
My guess is that the first version tries to create a new transition (with no delay or duration defined), whereas the second uses the transition passed in through the update() interface.
Strange is that:
what worked for another dependent animation did not for this.
the staggered delays and their associated durations were nevertheless accepted by the original version, allowing me to be misled by console logs..
Just to round this topic off, I can point out the the following (event-based) approach seems to work just as well as the tween variant with non-interpolable values documented above. I can switch freely between the two with no apparent difference in the resulting animations:
transition
.select("#chordinput") // id attribute of the input element
.each("start", setChordname( waveplot.chordname ));
Thug

Resources