cleaning axis in dynamic charts in dimple.js - d3.js

I'm using the clean axis function courtesy of #JohnKiernander. This works fine with static charts. But when I have a chart that updates (in this example when a button in clicked), the clean axis function does not work as expected. The function also erases others numbers of the axis. Is there a way to make this function work with dynamic charts? or do I have to take another approach?
See fiddle: http://jsfiddle.net/jdash99/oba54L1a/ for a better explanation.
// Clean Axis Function for reference
// Pass in an axis object and an interval.
var cleanAxis = function (axis, oneInEvery) {
// This should have been called after draw, otherwise do nothing
if (axis.shapes.length > 0) {
// Leave the first label
var del = 0;
// If there is an interval set
if (oneInEvery > 1) {
// Operate on all the axis text
axis.shapes.selectAll("text").each(function (d) {
// Remove all but the nth label
if (del % oneInEvery !== 0) {
this.remove();
// Find the corresponding tick line and remove
axis.shapes.selectAll("line").each(function (d2) {
if (d === d2) {
this.remove();
}
});
}
del += 1;
});
}
}
};

I suggest switching to a method with sets opacity rather than removing the label completely. I've modified your fiddle in 2 ways. Firstly the clean axis method becomes:
var cleanAxis = function (axis, oneInEvery) {
// This should have been called after draw, otherwise do nothing
if (axis.shapes.length > 0) {
// Leave the first label
var del = 0;
// If there is an interval set
if (oneInEvery > 1) {
// Operate on all the axis text
axis.shapes.selectAll("text").each(function (d) {
d3.select(this).attr("opacity", 1);
// Remove all but the nth label
if (del % oneInEvery !== 0) {
d3.select(this).attr("opacity", 0);
}
del += 1;
});
}
}
};
also because you are animating the draws you can't draw cleanAxis straight after, you need to assign it to the afterDraw property of the series instead:
s.afterDraw = function () { cleanAxis(myAxis, 10); };
This avoids a race condition on the label creation/hiding.
Here's the updated fiddle: http://jsfiddle.net/oba54L1a/2/

Related

d3fc - Crosshair with snapping using latest version 14

In previous version of d3fc my code was using fc.util.seriesPointSnapXOnly for snapping the crosshair.
This appears to be gone in the latest version of d3fc (or maybe I'm missing it in one of the standalone packages?).
I'm using the canvas implementation (annotationCanvasCrosshair) and it seems to also be missing the "snap" function where it was previously used like so:
fc.tool.crosshair()
.snap(fc.util.seriesPointSnapXOnly(line, series))
Additionally, "on" is also not available, so I can't attach events like trackingstart, trackingend, etc.
How can I implement a snapping crosshair now? The canvas version of the components are badly lacking examples. Does anyone have an example showing a snapping crosshair in the latest version of d3fc via canvas rendering?
Here's what I have so far https://codepen.io/parliament718/pen/xxbQGgp
I understand you've raised the issue with d3fc github, therefore I'll assume you are aware that util/snap.js is been deprecated.
Since this functionality unsupported now, it seems that the only feasible way to work around it will be to implement your own.
I took your pen and original snap.js code as starting point and applied the method outlined in Simple Crosshair example from the documentation.
I ended up having to add missing functions and their dependencies verbatim (surely you can refactor and package it up into a separate module):
function defined() {
var outerArguments = arguments;
return function(d, i) {
for (var c = 0, j = outerArguments.length; c < j; c++) {
if (outerArguments[c](d, i) == null) {
return false;
}
}
return true;
};
}
function minimum(data, accessor) {
return data.map(function(dataPoint, index) {
return [accessor(dataPoint, index), dataPoint, index];
}).reduce(function(accumulator, dataPoint) {
return accumulator[0] > dataPoint[0] ? dataPoint : accumulator;
}, [Number.MAX_VALUE, null, -1]);
}
function pointSnap(xScale, yScale, xValue, yValue, data, objectiveFunction) {
// a default function that computes the distance between two points
objectiveFunction = objectiveFunction || function(x, y, cx, cy) {
var dx = x - cx,
dy = y - cy;
return dx * dx + dy * dy;
};
return function(point) {
var filtered = data.filter(function(d, i) {
return defined(xValue, yValue)(d, i);
});
var nearest = minimum(filtered, function(d) {
return objectiveFunction(point.x, point.y, xScale(xValue(d)), yScale(yValue(d)));
})[1];
return [{
datum: nearest,
x: nearest ? xScale(xValue(nearest)) : point.x,
y: nearest ? yScale(yValue(nearest)) : point.y
}];
};
}
function seriesPointSnap(series, data, objectiveFunction) {
return function(point) {
var xScale = series.xScale(),
yScale = series.yScale(),
xValue = series.crossValue(),
yValue = (series.openValue).call(series);
return pointSnap(xScale, yScale, xValue, yValue, data, objectiveFunction)(point);
};
};
function seriesPointSnapXOnly(series, data) {
function objectiveFunction(x, y, cx, cy) {
var dx = x - cx;
return Math.abs(dx);
}
return seriesPointSnap(series, data, objectiveFunction);
}
The working end result can be seen here: https://codepen.io/timur_kh/pen/YzXXOOG. I basically defined two series and used a pointer component to update that second series data and trigger a re-render:
const data = {
series: stream.take(50), // your candle stick chart
crosshair: [] // second series to hold the crosshair position
};
.............
const crosshair = fc.annotationCanvasCrosshair() // define your crosshair
const multichart = fc.seriesCanvasMulti()
.series([candlesticks, crosshair]) // we've got two series now
.mapping((data, index, series) => {
switch(series[index]) {
case candlesticks:
return data.series;
case crosshair:
return data.crosshair;
}
});
.............
function render() {
d3.select('#zoom-chart')
.datum(data)
.call(chart);
// add the pointer component to the plot-area, re-rendering each time the event fires.
var pointer = fc.pointer()
.on('point', (event) => {
data.crosshair = seriesPointSnapXOnly(candlesticks, data.series)(event[0]);// and when we update the crosshair position - we snap it to the other series using the old library code.
render();
});
d3.select('#zoom-chart .plot-area')
.call(pointer);
}
UPD:
the functionality can be simplified like so, i also updated the pen:
function minimum(data, accessor) {
return data.map(function(dataPoint, index) {
return [accessor(dataPoint, index), dataPoint, index];
}).reduce(function(accumulator, dataPoint) {
return accumulator[0] > dataPoint[0] ? dataPoint : accumulator;
}, [Number.MAX_VALUE, null, -1]);
}
function seriesPointSnapXOnly(series, data, point) {
if (point == undefined) return []; // short circuit if data point was empty
var xScale = series.xScale(),
xValue = series.crossValue();
var filtered = data.filter((d) => (xValue(d) != null));
var nearest = minimum(filtered, (d) => Math.abs(point.x - xScale(xValue(d))))[1];
return [{
x: xScale(xValue(nearest)),
y: point.y
}];
};
This is far from polished, but I'm hoping it conveys the general idea.

dc.js exclude the brushed area and highlight rest

I'm not data-viz expert or d3, I have found plenty of examples to how to build brushing and zoom for example Mike.
They all have shown how to filter to the brushed area but I want to achieve to reverse of that effect, how?
Can someone through me ideas how to achieve it?
I don't know why I assumed you meant a bar chart when you linked to an area chart. You can ignore the highlighting section and skip to filtering if you're interested in doing this with line charts. There is no highlighting of line chart, just the brush itself.
Highlighting the bars in reverse
This isn't all that hard, but it's somewhat messy because we replace an undocumented function in the chart. Like most things in dc.js, if there isn't an option, you can usually replace the functionality (or add or change stuff once the chart has rendered/drawn).
Here there's a specific, public function which fades the deselected areas. It's called fadeDeselectedArea. (Actually it both fades and un-fades when the chart is ordinal, but we'll ignore that part.)
The original function looks like this:
_chart.fadeDeselectedArea = function () {
var bars = _chart.chartBodyG().selectAll('rect.bar');
var extent = _chart.brush().extent();
if (_chart.isOrdinal()) {
if (_chart.hasFilter()) {
bars.classed(dc.constants.SELECTED_CLASS, function (d) {
return _chart.hasFilter(d.x);
});
bars.classed(dc.constants.DESELECTED_CLASS, function (d) {
return !_chart.hasFilter(d.x);
});
} else {
bars.classed(dc.constants.SELECTED_CLASS, false);
bars.classed(dc.constants.DESELECTED_CLASS, false);
}
} else {
if (!_chart.brushIsEmpty(extent)) {
var start = extent[0];
var end = extent[1];
bars.classed(dc.constants.DESELECTED_CLASS, function (d) {
return d.x < start || d.x >= end;
});
} else {
bars.classed(dc.constants.DESELECTED_CLASS, false);
}
}
};
source link
We'll ignore the ordinal part because that's only individual selection, not brushed selection. Here is the reverse of the second part:
spendHistChart.fadeDeselectedArea = function () {
var _chart = this;
var bars = _chart.chartBodyG().selectAll('rect.bar');
var extent = _chart.brush().extent();
// only covering the non-ordinal (ranged brush) case here...
if (!_chart.brushIsEmpty(extent)) {
var start = extent[0];
var end = extent[1];
bars.classed(dc.constants.DESELECTED_CLASS, function (d) {
return d.x >= start && d.x < end;
});
} else {
bars.classed(dc.constants.DESELECTED_CLASS, false);
}
};
Creating a variable _chart is just to keep the code the same as much as possible. You can see that d.x >= start && d.x < end is exactly the opposite of d.x < start || d.x >= end
Reversing the filtering
We'll need to add a filterHandler to the chart in order to reverse the filtering. Again, we'll base it off the default behavior, but here there's a legitimate customization point so we don't have to replace a function, just supply one:
spendHistChart.filterHandler(function(dimension, filters) {
if(filters.length === 0)
dimension.filter(null);
else {
// assume one RangedFilter but apply in reverse
// this is less efficient than filterRange but it shouldn't
// matter much unless the data is huge
var filter = filters[0];
dimension.filterFunction(function(d) {
return !filter.isFiltered(d);
})
}
});
Again, we cut out the cases we don't care about. There is no reason to be general about something that has a specific purpose and it will only cause maintenance problems. The only two cases we care about are no filter and one range filter.
Here the RangedFilter already supplies a filter function, so we can just call it and not (!) the result. This will be slightly less efficient than the filterRange but crossfilter has no native support for multiple ranges (or the inverse of a range).
That's it! Fiddle here: http://jsfiddle.net/gordonwoodhull/46snsbc2/8/

Using Custom reduce function with Fake Groups

I have line chart where I need to show frequency of order executions over the course of a day. These orders are grouped by time interval, for example every hour, using custom reduce functions. There could be an hour interval when there were no order executions, but I need to show that as a zero point on the line. I create a 'fake group' containing all the bins with a zero count...and the initial load of the page is correct.
However the line chart is one of 11 charts on the page, and needs to be updated when filters are applied to other charts. When I filter on another chart, the effects on this particular frequency line chart are incorrect. The dimension and the 'fake group' are used for the dc.chart.
I put console.log messages in the reduceRemove function and can see that there is something wrong...but not sure why.
Any thoughts on where I could be going wrong.
FrequencyVsTimeDimension = crossfilterData.dimension(function (d) { return d.execution_datetime; });
FrequencyVsTimeGroup = FrequencyVsTimeDimension.group(n_seconds_interval(interval));
FrequencyVsTimeGroup.reduce(
function (p, d) { //reduceAdd
if (d.execution_datetime in p.order_list) {
p.order_list[d.execution_datetime] += 1;
}
else {
p.order_list[d.execution_datetime] = 1;
if (d.execution_type !== FILL) p.order_count++;
}
return p;
},
function (p, d) { //reduceRemove
if (d.execution_type !== FILL) p.order_count--;
p.order_list[d.execution_datetime]--;
if (p.order_list[d.execution_datetime] === 0) {
delete p.order_list[d.execution_datetime];
}
return p;
},
function () { //reduceInitial
return { order_list: {}, order_count: 0 };
}
);
var FrequencyVsTimeFakeGroup = ensure_group_bins(FrequencyVsTimeGroup, interval); // function that returns bins for all the intervals, even those without data.

Disable brush resize (DC.js, D3.js)

Brush extent needs to be changed only from a dropdown as shown here: https://jsfiddle.net/dani2011/67jopfj8/3/
Need to disable brush extending by:
1) Extending an existing brush using the handles/resize-area of the brush
Gray circles are the handels:
2) Dragging a new brush by clicking on the brush background, where the
haircross cursor appears.
JavaScript file
Removed the handles of the brush:
timeSlider.on('preRedraw',function (chart)
{
var timesliderSVG = d3.select("#bitrate-timeSlider-chart").selectAll("g.brush").selectAll("g.resize").selectAll("*").data(data[0]).exit().remove();})
If using css instead:
#bitrate-timeSlider-chart g.resize {
display:none;
visibility:hidden;
Now it looks like this:
.
The rect and the path elements inside "resize e","resize w" were removed:
However,the "resize e", "resize w" for extanding the brush still exist:
g.resize.e and g.resize.w dimesions are 0*0:
Furthurmore,after deleting "resize e","resize w" in the "developer tools/elements" in chrome,they reappear after moving the brush.
Tried to remove the resize-area in brushstart,brush,brushend:
timeSlider.on('renderlet', function (chart) {
var brushg = d3.select("#bitrate-timeSlider-chart").selectAll("g.brush");
var resizeg = brushg.selectAll("g.resize").selectAll("*").data(data[0]);
var timesliderSVG4 =
brushg.on("brushstart", function () {resizeg.exit().remove()}).on("brush", function () { resizeg.exit().remove() }).on("brushend", function () {resizeg.exit().remove() })
dc.js file
Tried to change setHandlePaths,resizeHandlePath:
1)
Remarked the _chart.setHandlePaths(gBrush):
_chart.renderBrush = function (g) {....
// _chart.setHandlePaths(gBrush);
...}
2) Changed _chart.setHandlePaths = function (gBrush) for example by removing the gbrush.path:
// gBrush.selectAll('.resize path').exit().remove();
3) Remarked/changed _chart.resizeHandlePath = function (d) {...}.
d3.js file
1) Remarked/changed resize such as:
mode: "move" //mode: "resize" ,
var resize = g.selectAll(".resize").data(resizes[0], d3_identity);
Using resizes[0] disable the brush rendering on the background but still can re-extend the existing brush
2) Remarked/changed d3_svg_brushResizes
3) In d3.svg.brush = function():
a) Added .style("display","none"):
background.enter().append("rect").attr("class", "background").style("visibility", "hidden").style("display", "none").style("cursor", "none");
b) background.exit().remove();
The cursor now is "pointer" instead of "haircross" extending the brush to a full width
c) d3_svg_brushCursor disabled makes the whole brush disappear
4) Changed the pointer-events as specified here: https://developer.mozilla.org/en/docs/Web/CSS/pointer-events
5) console.log in different places to track the different brush events:
function d3_event_dragSuppress(node) {
console.log("here2 ");
}
if (d3_event_dragSelect) {
console.log("here3 d3_event_dragSelect");
...
}
return function (suppressClick) {
console.log("suppressClick1");
...
var off = function () {
console.log("suppressClick2");
...
w.on(click, function () {
console.log("suppressClick3")
...
function d3_mousePoint(container, e) {
console.log("d3_mousePoint1")
...
if (svg.createSVGPoint) {
console.log("createSVGPoint");
...
if (window.scrollX || window.scrollY) {
console.log("createSVGPoint1");
svg = d3.select("body").append("svg").style({
...
function dragstart(id, position, subject, move, end) {
console.log("dragstart")
...
function moved() {
console.log("moved ");
console.log("transition1");
...
if (d3.event.changedTouches) {
console.log("brushstart1");
...
} else {
console.log("brushstart2");
..
if (dragging) {
console.log("dragging4");
....
if (d3.event.keyCode == 32) {
if (!dragging) {
console.log("notdragging1");
...
function brushmove() {
console.log("brushmove");
...
if (!dragging) {
console.log("brushmove!dragging");
if (d3.event.altKey) {
console.log("brushmove!dragging1");
...
if (resizingX && move1(point, x, 0)) {
console.log("resizeXMove1");
...
if (resizingY && move1(point, y, 1)) {
console.log("resizeYMove1");
...
if (moved) {
console.log("moved");
...
}
function move1(point, scale, i) {
if (dragging) {
console.log("dragging1");
...
if (dragging) {
console.log("dragging2");
...
} else {
console.log("dragging10");
...
if (extent[0] != min || extent[1] != max) {
console.log("dragging11");
if (i) console.log("dragging12"); yExtentDomain = null;
console.log("dragging13");
function brushend() {
console.log("brushend");
...
The two changes that seemed to get closest to the needed result are in d3.js:
1) Using resizes[0] disables the brush rendering on the background but still can re-extend the existing brush
var resize = g.selectAll(".resize").data(resizes[0], d3_identity);
2) Removing the brush's background changes the cursor to "pointer" instead of "haircross",extending the brush to a full width only when clicking on the graph
`background.exit().remove();`
Any help would be very appreciated!
This is from the accepted answer in Disable d3 brush resize, as suggested by #altocumulus. I didn't see a response from #Dani on this idea in particular, but thought I'd go ahead and try it, since I've seen other people try it in the past. (Probably on the dc.js users group.)
It looks a little twitchy, because d3.js will draw the brush at the new extent, and then a moment later we reset the extent to what we want, but functionally it seems to do what we want.
In dc.js the function that handles brush "rounding" is coordinateGridMixin.extendBrush:
_chart.extendBrush = function () {
var extent = _brush.extent();
if (_chart.round()) {
extent[0] = extent.map(_chart.round())[0];
extent[1] = extent.map(_chart.round())[1];
_g.select('.brush')
.call(_brush.extent(extent));
}
return extent;
};
Notice that it's following the same pattern as Lars' answer. Well, this is sort of like rounding, right? Let's override it.
First, let's store the current number of hours when it's set through the dropdown:
var graphSpan;
function addHours(amountHours) {
graphSpan = amountHours;
// ...
Next let's override coordinateGridMixin.extendBrush:
timeSlider.extendBrush = function() {
var extent = timeSlider.brush().extent();
if(graphSpan) {
extent[1] = moment(extent[0]).add(graphSpan, 'hours');
}
return extent;
}
We just replace the function. If we needed to reuse the old implementation in our function, we could use dc.override.
If graphSpan has been set, we add that amount to the beginning to get the end. If it's not set, we allow the user to specify the brush width - you'd need to default the graphSpan and the select widget if you wanted that part to work automatically.
Well, let's admit it: it's very twitchy, but it works:
EDIT: Got rid of the twitch! The problem was that dc.js was setting the brush extent asynchronously after a little while (in response to the filter event). If we also set it during extentBrush then it never shows the wrong extent:
timeSlider.extendBrush = function() {
var extent = timeSlider.brush().extent();
if(graphSpan) {
extent[1] = moment(extent[0]).add(graphSpan, 'hours');
timeSlider.brush().extent(extent);
}
return extent;
}
https://jsfiddle.net/gordonwoodhull/xdo05chk/1/
What worked for me:
in d3:
disable resize handles
d3.selectAll('.brush>.handle').remove();
disable crosshair
d3.selectAll('.brush>.overlay').remove();
or
in css:
disable resize handles -
.handle {
pointer-events: none;
}
disable crosshair -
.overlay {
pointer-events: none;
}

hammer.js detect variables in panmove and unbind when it hits certain criteria

my goal is detect when an element has reached a certain margin-left, and than unbind or stop the panmove from continuing if it hits that threshold.
I have a "panmove" bound to an element using hammer.js, and jquery hammer plugin.
I noticed that in the panmove, console.log(e) will fire hundreds of times as you move the elements, which is expected. If you however put an if statement in the panmove function, it only goes off of the initial state of the first panmove and not the current one.
.bind("panmove", function (e) {
var count = 0;
console.log(e);
console.log(count++);
var _this = $(e.target);
var _thisDataLeft = _this.attr("data-left");
var _thisDataMaxLeft = _this.attr("data-maxleft"); // this is derived from the width of the Delete box, which can be any width.
if (Math.abs(_thisDataLeft) < Number(_thisDataMaxLeft)) {
_this.css({ left: Number(_thisDataLeft) + e.gesture.deltaX }); // controls movement of top layer
console.log(count++);
}
I noticed that the console.log(count++) always fires 1, instead of iterating up, as if it is only reading it once in the beginning.
How can I run an if statement inside of this Pan, so that it is always the current information, and not just the first iteration?
Ended up moving away from Hammer.js, was not able to get the results I needed. It looks like the more basic jquery.event.move.js was easier to use than hammer.
here is my example in js fiddle
https://jsfiddle.net/williamhowley/o9uvo50y/
$(document).ready(function () {
// http://stephband.info/jquery.event.move/
// http://stephband.info/jquery.event.swipe/
// add swipe functionality to the rows.
// I think you will need to add the swipe left, after it is activated by a HOLD down press.
// idk, how do you always make something swipable.
var wrap = $('ul#main');
$('ul#main > li')
.on('movestart', function (e) {
console.log("move start");
// var $li = $(e.target).closest('.swipable'); // this would be normal live integration
var $li = $(e.target);
if ($li.attr("data-hasplaceholder") !== "true") { // if it does not have a placeholder, add one.
createBackgroundSpacer($li);
$li.attr("data-hasplaceholder", true); // signify that a placeholder has been created for this element already.
}
// If the movestart heads off in a upwards or downwards
// direction, prevent it so that the browser scrolls normally.
if ((e.distX > e.distY && e.distX < -e.distY) ||
(e.distX < e.distY && e.distX > -e.distY)) {
e.preventDefault();
return;
}
// To allow the slide to keep step with the finger,
// temporarily disable transitions.
wrap.addClass('notransition'); // add this to the container wrapper.
})
.on('move', function (e) {
// event definitions
// startX : 184, where from left the mouse curser started.
// deltaX: ?
// distX: how far the mouse has moved, if negative moving left. Still need to account for double movement, currently can only handle one movement.
console.log("move");
console.log(e);
var maxLeft = $('.rightContent').width();
var marginLeftNum = Number($(this).css('margin-left').replace(/[^-\d\.]/g, ''));
if (marginLeftNum <= -maxLeft && e.deltaX < 0) { // Case when user is at outermost left threshold, and trying to move farther left.
$(this).css({ 'margin-left': -maxLeft });
}
else if (marginLeftNum == -maxLeft && e.deltaX > 0) { // When user is at threshold, and trying to move back right.
$(this).css({ 'margin-left': marginLeftNum + e.deltaX });
}
else if (e.target.offsetLeft>=0 && e.deltaX>0) { // If the offset is 0 or more, and the user is scrolling right (which is a positive delta, than limit the element. )
$(this).css({ 'margin-left': 0 });
}
// Must have a Negative offset, and e.deltaX is Negative so it is moving left.
else if (e.deltaX < 0) { // Case when element is at 0, and mouse movement is going left.
$(this).css({ 'margin-left': marginLeftNum + e.deltaX });
}
else { // Moving Right when not on 0
$(this).css({ 'margin-left': marginLeftNum + e.deltaX });
}
})
.on('swipeleft', function (e) {
console.log("swipeleft");
})
.on('activate', function (e) {
// not seeing this activate go off, i think this is custom function we can add on if swipe left hits a threshold or something.
console.log("activate");
})
.on('moveend', function (e) {
console.log("move end");
wrap.removeClass('notransition');
});
var createBackgroundSpacer = function ($shoppingListRow) {
var border = 2;
$shoppingListRow.css({ 'width': $shoppingListRow.width() + border, 'height': $shoppingListRow.height() + border }); // gives itself set width and height
$shoppingListRow.addClass('swipable');
// placeholder HTML
var leftPlaceholder = $('<div class="leftPlaceholder"></div>').css({ 'height': $shoppingListRow.height()});
var rightPlaceholder = $('<div class="rightPlaceholder"></div>')
var rightContent = $('<div class="rightContent">Delete</div>').css({ 'height': $shoppingListRow.height()});
rightPlaceholder.append(rightContent);
var placeHolder = $('<div class="swipePlaceholder clearfix"></div>'); // goes around the two floats.
placeHolder.css({ 'width': $shoppingListRow.width(), 'height': $shoppingListRow.height() });
placeHolder.append(leftPlaceholder, rightPlaceholder);
$shoppingListRow.before(placeHolder); // adds placeholder before the row.
$shoppingListRow.css({ 'marginTop': -($shoppingListRow.height() + border) });
};
});

Resources