I'm working on a new chart type for NVD3 called lineWithFocusPlusSecondary. It has two graphs on top of each other. It's working well except for one problem: if the x values are dates, when you zoom in, the graph gets cut off in an unpleasant manner. This doesn't happen with the default lineChart so I've definitely done something wrong.
I've put my code in this plnkr: https://plnkr.co/edit/9GzI0Jxi5qXZas3ljuBQ?p=preview
Would love some help :) It seems like the issue in the screenshot is that the x-axis domain goes until ~7:05pm but we don't have a data point until 7pm.
It could be something something to do with my onBrush function:
function onBrush(extent) {
var processedData = processData(container.datum()),
dataPrimary = processedData.dataPrimary,
dataSecondary = processedData.dataSecondary,
seriesPrimary = processedData.seriesPrimary,
seriesSecondary = processedData.seriesSecondary;
updateChartData(
getIntegerExtent(extent),
dataPrimary,
dataSecondary,
seriesPrimary,
seriesSecondary
);
}
function getIntegerExtent(extent) {
return [Math.ceil(extent[0]), Math.floor(extent[1])];
}
function updateAxes(extent) {
primaryXAxis.scale(primaryChart.xScale());
primaryXAxis.domain(extent);
g
.select('.nv-primary .nv-x.nv-axis')
.transition()
.duration(transitionDuration)
.call(primaryXAxis);
g
.select('.nv-secondary .nv-ySecondary.nv-axis')
.transition()
.duration(transitionDuration)
.call(yAxisSecondary);
g
.select('.nv-primary .nv-yPrimary.nv-axis')
.transition()
.duration(transitionDuration)
.call(yAxisPrimary);
}
function updateChartData(currentExtent, dataPrimary, dataSecondary) {
updateAxes(currentExtent);
var primaryDatasetsWithinBrushExtent = !dataPrimary.length
? [
{
values: []
}
]
: dataPrimary.map(function(d) {
var restrictedDataset = Object.assign({}, d);
restrictedDataset.values = d.values.filter(function(d, i) {
return (
primaryChart.x()(d, i) >= currentExtent[0] &&
primaryChart.x()(d, i) <= currentExtent[1]
);
});
return restrictedDataset;
});
var primaryChartWrap = g
.select('.nv-primary .nv-linesWrap')
.datum(primaryDatasetsWithinBrushExtent);
var secondaryDatasetsWithinExtent = !dataSecondary.length
? [
{
values: []
}
]
: dataSecondary.map(function(d) {
var restrictedDataset = Object.assign({}, d);
restrictedDataset.values = d.values.filter(function(d, i) {
return (
secondaryChart.x()(d, i) >= currentExtent[0] &&
secondaryChart.x()(d, i) <= currentExtent[1]
);
});
return restrictedDataset;
});
var focusSecondaryChartWrap = g
.select('.nv-secondary .nv-secondaryChartWrap')
.datum(secondaryDatasetsWithinExtent);
primaryChart.xDomain(currentExtent);
secondaryChart.xDomain(currentExtent);
primaryChartWrap
.transition()
.duration(transitionDuration)
.call(primaryChart);
focusSecondaryChartWrap
.transition()
.duration(transitionDuration)
.call(secondaryChart);
}
I discovered the issue was that I was trying to set the xDomain in multiple locations. This seems to mess up NVD3's logic. After I removed all of the .domain/.xDomain it worked perfectly :)
Debugging approach was to carefully read through the lineChart.js code and notice what it didn't have that I had.
Related
I am new to D3 and trying to use zoom functionality.
I have a 100 thousand plus points to be displayed and inorder to not do break my browser, I am trying to randomly select 5000 points and show the user, and on zooming in - query more relevant points from the database.
I do not want to do this on every zoom , my zoom scaleExtent is set to [1,5] and every time the zoomscale is 3 or 5 I will query data from database and refresh the plot, this works fine, just a problem every time there is a plot refresh the scales are set to [1,5] again and it goes in an endless loop. And zoom out completely breaks.
Anybody have any ideas how to approach doing this.
I tried to create to buttons for zoomin and zoomout and call the function as below onClick them
function initiateZoom(val)
{
var direction = 1
direction = (val === 'zoom_in') ? 1 : -1;
extent = zoom.scaleExtent();
incrZoom = parseInt(lastZoomScale) + parseInt(direction);
console.log("Target Zoom Scale"+ incrZoom);
if (incrZoom < extent[0] || incrZoom > extent[1])
{
if(incrZoom < extent[0])
{
incrZoom = parseInt(incrZoom) + direction;
}else {
incrZoom = parseInt(incrZoom) + direction;
}
alert("Min/Max Reached");
return false;
}
zoom.scaleTo(scatter_plt_svg, (incrZoom));
}
And on zoom call the function zoomed
function zoomed() {
var zoom_scale = (d3.event.transform.k);//.toFixed(2);
var new_xScale= null;
var new_yScale = null;
new_xScale = d3.event.transform.rescaleX(xScl);
new_yScale = d3.event.transform.rescaleY(yScl);
// update axes
gX.call(d3.axisBottom(new_xScale).ticks(10));
gY.call(d3.axisLeft(new_yScale).ticks(10));
circles.data(plotdata)
.attr('cx', function (d, i) {
return new_xScale(d[1])
})
.attr('cy', function (d, i) {
return new_yScale(d[2])
});
curr_xScale = new_xScale;
curr_yScale = new_yScale;
lastZoomScale = zoom_scale;
//If zoom scale is a value of scaleExtent, refresh plot.
if(zoom_scale == 2 || zoom_scale == 4 || zoom_scale == 6 || zoom_scale == 8 || zoom_scale == 10)
{
scatter_plt_svg.on('.zoom', null);
initialize_plots(false,"zoom"); //helps get data from server and refresh plots.
}
}
Regards
Tasneem
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.
I have been using the d3 v5 .join update pattern. It's great, but I have a situation in my code where the data is not being passed down as expected and I am at a loss for what is going wrong. My code looks like this :
let binnedWrap = wrap.selectAll('.attr-wrap').data(sortedBins).join('g').attr('class', d=> d.key + ' attr-wrap');
binnedWrap.attr('transform', (d, i)=> 'translate(0,'+(i * (height + 5))+')');
let label = binnedWrap.append('text').text(d=> d.key).attr('y', 40).attr('x', 80).style('text-anchor', 'end');
let branchGroup = binnedWrap.selectAll('g.branch-bin').data(d=> {
///THIS IS RIGHT
console.log('data before the branch bins',d);
return d.branches}).join('g').classed('branch-bin', true);
branchGroup.attr('transform', (d, i)=> 'translate('+(100 + branchScale(i))+')');
This works as expected. The data that consoles is correct and it creates a group classed 'branch-bin' for each branch element in d.branches
BUT- when I attempt to use the branch data within each of the 'branch-bin' group, I am not getting the expected d.branches data:
let continDist = branchGroup.filter(f=> f.type === 'continuous');
var lineGen = d3.line()
.y((d, i)=> {
console.log('y',d, i)
let y = d3.scaleLinear().domain([0, 16]).range([0, height]);
return y(i);
})
.x(d=> {
let x = d3.scaleLinear().domain([0, 100]).range([0, 100]);
return x(Object.entries(d).length - 2);
});
//THIS IS RIGHT//
console.log(continDist.data())
continDist.append('path').data((d, i)=> {
console.log('supposed to be data in d branch', d, i);
return lineGen(d.bins)});
The output looks the same as the above console. The path is not being passed the branch data.
Any idea what is going on here would be much appreciated!
I made the mistake of using
continDist.append('path').data((d, i)=>lineGen(d.bins));
instead of continDist.append('path').attr('d', (d, i)=>lineGen(d.bins));
after assigning d as an attr of path it worked as expected.
I rendered a stacked bar and then I'm trying to re-draw the chart with new filter on clicking a button. Now, as I click the button the previous axes remains but the chart disappears and doesn't re-draw. I don't know what I'm missing here..
Here is the JSFiddle for this stackedBar.
function myFunc(){
var dimForHour = cf.dimension(function(d) { return d.date; });
dimForHour.filter([hour, today]);
var dimByChannel = cf.dimension(function(d) { return d.channelUUID; });
var groupAvgChan = dimByChannel.group().reduce(
function reduceAdd(p, v) {
p.bytesTxd = p.bytesTxd + v.bytesTxd;
p.count = p.count + 1;
p.avg = p.bytesTxd / p.count;
p.avgTot = p.avgTot + p.avg;
p.avgPrcnt = (p.avg / p.avgTot) * 100;
if(p.max < v.bytesTxd) { p.max = v.bytesTxd; }
p.maxTot = p.maxTot + p.max;
p.maxPrcnt = (p.max / p.maxTot) * 100;
return p;
},
function reduceRemove(p, v) {
p.bytesTxd = p.bytesTxd - v.bytesTxd;
p.count = p.count - 1;
p.avg = p.bytesTxd / p.count;
p.avgTot = p.avgTot - v.avgTot;
p.avgPrcnt = (p.avg / p.avgTot) * 100;
if(p.max > v.bytesTxd) { p.max = v.bytesTxd; }
p.maxTot = p.maxTot - p.max;
p.maxPrcnt = (p.max / p.maxTot) * 100;
return p;
},
function reduceInitial() {
return {
bytesTxd:0, count:0, avg:0, avgTot:0,
avgPrcnt:0, max:0, maxTot:0,maxPrcnt:0};
});
/*chanUtil = dc.barChart("#chanUtil")
chanUtil
.dimension(dimByChannel)
.group(groupAvgChan,"Avg Utilization %").valueAccessor(function(d) { return d.value.avgPrcnt; })
.stack(groupAvgChan,"Max Utilization %", function(d) { return d.value.maxPrcnt; })
.x(d3.scale.ordinal().domain(data.map(function (d) { return d.channelUUID })))
.xUnits(dc.units.ordinal);*/
dc.redrawAll();
}
I'm not sure what you're trying to calculate, but the problem seems to arise from this line:
p.avgTot = p.avgTot - v.avgTot;
which does not match the corresponding
p.avgTot = p.avgTot + p.avg;
v does not have a member avgTot, so you get NaN, and everything breaks from there. The best way to debug these kinds of things is
put a breakpoint on the redraw to see what values are in the group
put breakpoints inside the reduce functions to see what went wrong in the calculation.
Changing the reduceRemove function to use
p.avgTot = p.avgTot - p.avg;
seems to fix the problem (at least, there are still bars after the button is clicked).
Fork of your fiddle: http://jsfiddle.net/gordonwoodhull/0vvh1xex/6/
I also added elasticY(true) to be able to see the change better. And yes, the upward transition on bars is particularly wrong here; if you are able to upgrade to dc.js 2.0 (which is on beta 6), that is fixed.
Note: you have some dead code inside myFunc: it looks like you are recreating dimByChannel and groupAvgChan exactly the same, and then not using them for anything.
I have a linear scale and I am using it for one of my axis in a chart.
I want to define a custom format only showing positive numbers and following this example I created a customFormat in this way:
function yFormat(formats) {
return function(value) {
var i = formats.length - 1, f = formats[i];
while (!f[1](value)) f = formats[--i];
return f[0](value);
};
}
var customFormat = yFormat([
[d3.format("?????"), function() { return true }],
[d3.format("d"), function(d) { return d >= 0 }]
]);
but I don't know how to define an empty format for Numbers (link to doc)
Does anybody have an idea? Thanks
EDIT
d3.format("") seems to work for time scales but not for linear scale. In a console:
> var emptyTime = d3.time.format("");
undefined
> emptyTime("something");
""
> var emptyInt = d3.format("");
undefined
> emptyInt(5);
"5"
ANSWER
I am just using tickValues on the axis itself. So, something like:
yAxis.tickValues(
d3.range(
0,
d3.max(y.domain()),
shift
)
);