So I have a SeriesChart with this code:
'use strict';
var sectorChart = new dc.SeriesChart('#test-chart');
d3.json('php/query.php?option=sectors').then(data => {
const dateFormatSpecifier = '%Y-%m-%d';
const dateFormat = d3.timeFormat(dateFormatSpecifier);
const dateFormatParser = d3.timeParse(dateFormatSpecifier);
const numberFormat = d3.format('.2f');
var minDate = new Date();
var maxDate = new Date();
var minRatio = .5
var maxRatio = .5
data.forEach(d => {
d.dd = dateFormatParser(d.date);
d.ratio = +d.ratio; // coerce to number
if (d.dd < minDate ) minDate = d.dd;
if (d.dd > maxDate ) maxDate = d.dd;
if (d.ratio < minRatio ) minRatio = d.ratio
if (d.ratio > maxRatio ) maxRatio = d.ratio
});
const ndx = crossfilter(data);
const all = ndx.groupAll();
// Dimension is an array of sector and date.
// Later we'll take the date for the y-axis
// and the sector for the labels
const dateDimension = ndx.dimension(d => [d.name, d.dd]);
// group by the average, the value we'll plot
var avgGroup = dateDimension.group().reduceSum(d => d.average);
sectorChart /* dc.lineChart('#monthly-move-chart', 'chartGroup') */
.width(990)
.height(500)
.chart(c => new dc.LineChart(c))
.x(d3.scaleTime().domain([minDate, maxDate]))
.y(d3.scaleLinear().domain([minRatio, maxRatio]))
.margins({top: 30, right: 10, bottom: 20, left: 40})
.brushOn(false)
.yAxisLabel("ratio avg 10")
.renderHorizontalGridLines(true)
.dimension(dateDimension)
.group(avgGroup)
.seriesAccessor(d => d.key[0])
.keyAccessor(d => d.key[1])
.valueAccessor(d => +d.value)
.legend(dc.legend().x(450).y(15).itemHeight(13).gap(5).horizontal(1).legendWidth(340).autoItemWidth(true));
dc.renderAll();
});
It works all right, but now I would like to add a black horizontal line at 0.5 value. I guess I could modify the query to return a fictious "name" to the dataset with all values at 0.5, but that won't allow me to control the color, and anyway I would like to know if there is a better way, not to mess with the returned data.
Edit: According to Gordon's info, I have transposed as good as I could the vertical line to a horizontal line. It has worked, with some eccentricities. One, the line starts at the left side of the viewport, not on the y axis as it should. Two, and more mysterious, the line is drawn at 0.51 instead of 0.50. I have created a fiddle if anybody wants to play with it.
Fiddle
Based on Gordon's solution I have written a function, with the idea not to burden the chart's definition with all that code. Not fully happy with it, particularly the use of random, but anyways, the world keeps turning and other things to do. In any case, I leave it here in case somebody find it useful.
/**
* Draws a fixed line horizontally or vertically in a dc.js chart
* To be used in the "pretransition" event
* #example
* .on('pretransition', function (chart) {
* dcUtilFixedLine(chart, true, 0.5, [{attr: 'stroke', value: 'red'}] )
* }
* #param (Chart} [chart] The chart provided by the triggered event
* #param (Boolean) [horizontal] True if the line is horizontal, false if vertical
* #param (Object) [value] Value of the point where the line is to be inserted,
* usually the type of the dimension if vertical or group if horizontal
* #param (Array) [attributes] Array of Objects with attr/value pairs)
* giving the attributes added to the line.
* It defaults to stroke = black, stroke-width = 1.
*/
function dcUtilFixedLine(chart, horizontal, value, attributes ) {
if (horizontal) {
var extra_data = [
{y: chart.y()(value) + chart.margins().top, x: chart.margins().left},
{y: chart.y()(value) + chart.margins().top, x: chart.margins().left + chart.effectiveWidth()}
]
} else {
var extra_data = [
{x: chart.x()(value) + chart.margins().left, y: chart.margins().top},
{x: chart.x()(value) + chart.margins().left, y: chart.margins().top + chart.effectiveHeight()}
]
}
var addedPath = 'extraLine' + Math.random().toString().substr(2, 8)
var line = d3.line().x(d => d.x ).y(d => d.y );
var chartBody = chart.select('g');
var path = chartBody.selectAll('path.'+addedPath).data([extra_data]);
path = path.enter()
.append('path')
.attr('class', addedPath)
.attr('id', 'oeLine')
.attr('stroke', 'black')
.merge(path);
attributes.forEach( attribute => path.attr(attribute.attr, attribute.value))
path.attr('d', line);
}
And forked and added it to the new fiddle
Although you can add artificial data series in order to display extra lines, it's often easier to "escape to D3", especially if the lines are static and don't move.
The row vertical line example shows how to do this.
The basic outline is:
use the pretransition event to draw something each time the chart is updated
create two x/y points of data to draw, using the chart's scales and margins to determine pixel coordinates
select a path with a unique class name (here .extra) and join the data to it
For a horizontal line, it could look like this:
.on('pretransition', function(chart) {
var y_horiz = 0.5;
var extra_data = [
{y: chart.y()(y_horiz) + chart.margins().top, x: chart.margins().left},
{y: chart.y()(y_horiz) + chart.margins().top, x: chart.margins().left + chart.effectiveWidth()}
];
var line = d3.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; });
var chartBody = chart.select('g');
var path = chartBody.selectAll('path.extra').data([extra_data]);
path = path.enter()
.append('path')
.attr('class', 'extra')
.attr('stroke', 'black')
.attr('id', 'oeLine')
.attr("stroke-width", 1)
.merge(path);
path.attr('d', line);
});
Each X coordinate is offset by chart.margins().left, each Y coordinate by chart.margins().top.
The width of the chart in pixels is chart.effectiveWidth() and the height in pixels is chart.effectiveHeight(). (These are just the .width() and .height() with margins subtracted.)
Fork of your fiddle
(Note: the example apparently has a typo, using oeExtra instead of extra in the join. This could cause multiple lines to be drawn when the chart is updated. The class in the select should match the class in the join.)
Related
I'm trying to create a running line chart where it animates changes and the X and Y axis and the line itself.
I've started with the basic animation explanation at:
https://observablehq.com/#d3/learn-d3-animation?collection=#d3/learn-d3
And created my own (see code and link below)
My problem is that as soon as I add the await chartBug.update the Y axis starts flickering.
See animated gif below:
I think I need the await, to wait for the animation to complete before I start the next animation, and control the smooth speed of the animation.
While I'm here I would like also to ask how can I force the axis to draw the start and end ticks.
Thanks for the help
Here's a link to the page on www.observablehq.com
Here's the Animated gif of the problem - the source code is below:
chartBug = {
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);
const zx = x.copy(); // x, but with a new domain.
const zy = y.copy(); // x, but with a new domain.
const line = d3
.line()
.x(d => zx(d.date))
.y(d => y(d.close));
const path = svg
.append("path")
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("stroke-miterlimit", 1)
.attr("d", line(data));
const gx = svg.append("g").call(xAxis, zx);
const gy = svg.append("g").call(yAxis, y);
return Object.assign(svg.node(), {
update(step) {
const partialData = [];
for (let i = 0; i < step; i++) {
partialData.push(data[i]);
}
const t = svg.transition().duration(50);
zx.domain(d3.extent(partialData, d => d.date));
gx.transition(t).call(xAxis, zx);
zy.domain([0, d3.max(partialData, d => d.close)]);
gy.transition(t).call(yAxis, zy);
path.transition(t).attr("d", line(data));
return t.end();
}
});
}
{
replay2;
for (let i = 0, n = data.length; i < n; ++i) {
await chartBug.update(i);
yield i;
}
}
the issue seems to be on your yAxis function, every new update and rescale yAxis is recreated and the domain is redraw and removed, if you don't mind keep it you can remove call(g => g.select(".domain").remove() from it.
You need to await to update after the transition is done. I think this also answer your other question
yAxis = (g, scale = y) => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(scale).ticks(height / 40))
I would like to create D3 bubble charts by placing the creation in an object and calling instances of the object.
Outside of an object the script works well, showing scarttered bubbles, distributed among the canvas.
However, when I attempt to place the script in an object constructor, the forceSimulation function doesn't seem to scattered the bubbles on about the y axis. They simply gather together at the same cy and cx
this.forceStrength = 0.03;
this.width = 940;
this.height = 600;
this.center = { x: this.width / 2, y: this.height / 2 };
this.svg = null;
this.bubbles = null;
this.nodes = [];
this.charge = function(d) {
return -Math.pow(d.radius, 2.0) * this.forceStrength;
}
this.simulation = d3.forceSimulation()
.velocityDecay(0.2) //controls the friction at each "tick"
.force('x', d3.forceX().strength(this.forceStrength).x(this.center.x))
.force('y', d3.forceY().strength(this.forceStrength).y(this.center.y))
.force('charge', d3.forceManyBody().strength(this.charge))
.on('tick',this.ticked)
.on('end', function() {
// layout is done
console.log("end")
});
UPDATE Here is a jsfiddle of the full example code
I am trying to find the nearest neighbors in a scatterplot using the data attached below, with the help of this snippet -
const voronoiDiagram = d3.voronoi()
.x(d => d.x)
.y(d => d.y)(data);
data.forEach(function(d){
console.log(d, voronoiDiagram.find(d.x, d.y, 50));
});
Now the dataset I am using is the standard iris sepal, petal lengths data in
format -
{"sepalLength":7.7,"sepalWidth":3,"petalLength":"6.1","petalWidth":"2.3","species":"virginica","index":135,"x":374.99999999999994,"y":33.75,"vy":0,"vx":0},
{"sepalLength":6.3,"sepalWidth":3.4,"petalLength":"5.6","petalWidth":"2.4","species":"virginica","index":136,"x":524.9999999999999,"y":191.25,"vy":0,"vx":0},
{"sepalLength":6.4,"sepalWidth":3.1,"petalLength":"5.5","petalWidth":"1.8","species":"virginica","index":137,"x":412.5,"y":179.99999999999994,"vy":0,"vx":0},
{"sepalLength":6,"sepalWidth":3,"petalLength":"4.8","petalWidth":"1.8","species":"virginica","index":138,"x":374.99999999999994,"y":225,"vy":0,"vx":0},
....
So, essentially it is in the form of
{d: {x, y, sepal length, width, petal length, width}.
Now, I am trying to find the nearest neighbors with d3 voronoi from reference.
But, all I get is this in results -
Let point d in my dataset =
{"sepalLength":5.9,"sepalWidth":3,"petalLength":"5.1","petalWidth":"1.8","species":"virginica","index":149,"x":374.99999999999994,"y":236.24999999999997,"vy":0,"vx":0}
Now, the voronoiDiagram.find(d.x, d.y, 50) for this is resulting in -
"[375,236.25]"
That is, the same point with coordinates rounded off instead of another point.
So, how do I exclude current point being scanned in this case from the voronoi diagram.
Also, If I exclude that point & re-calculate everything would this be good from the performance perspective ?
Can anyone help me with finding nearest neighbors from a set of points
with d3 voronoi / quadtrees (I have tried a couple of examples already from Mike Bostock but couldn't get them to work in my case because of some errors,
so will post them if d3 voronoi does not help).
voronoiDiagram.find(y, x, r) will only ever return, at most, once cell. From the API documentation:
Returns the nearest site to point [x, y]. If radius is specified, only sites within radius distance are considered. (link)
I've previously read that as being plural, apparently I've never looked closely (and I think there is a large amount of utility in being able to find all points within a given radius).
What we can do instead is create a function fairly easily that will:
start with voronoiDiagram.find() to find the cell the point falls in
find the neighbors of the found cell
for each neighbor, see if its point is within the specified radius
if a neighbors point is within the specified radius:
add the neighbor to a list of cells with points within the specified radius,
use the neighbor to repeat steps 2 through 4
stop when no more neighbors have been found within the specified radius, (keep a list of already checked cells to ensure none are checked twice).
The snippet below uses the above process (in the function findAll(x,y,r)) to show points within the specified distance as orange, the closest point will be red (I've set the function to differentiate between the two).
var width = 500;
var height = 300;
var data = d3.range(200).map(function(d) {
var x = Math.random()*width;
var y = Math.random()*height;
var index = d;
return {x:x,y:y,index:index}
});
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height);
var circles = svg.selectAll()
.data(data, function(d,i) { return d.index; });
circles = circles.enter()
.append("circle")
.attr("cx",function(d) { return d.x; })
.attr("cy",function(d) { return d.y; })
.attr("r",3)
.attr("fill","steelblue")
.merge(circles);
var voronoi = d3.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.size([width,height])(data);
var results = findAll(width/2,height/2,30);
circles.data(results.nearest,function(d) { return d.index; })
.attr("fill","orange");
circles.data([results.center],function(d) { return d.index; })
.attr("fill","crimson");
var circle = svg.append("circle")
.attr("cx",width/2)
.attr("cy",height/2)
.attr("r",30)
.attr("fill","none")
.attr("stroke","black")
.attr("stroke-width",1);
circle.transition()
.attrTween("r", function() {
var node = this;
return function(t) {
var r = d3.interpolate(30,148)(t);
var results = findAll(width/2,height/2,r);
circles.data(results.nearest,function(d) { return d.index; })
.attr("fill","orange");
return r;
}
})
.duration(2000)
.delay(1000);
function findAll(x,y,r) {
var start = voronoi.find(x,y,r);
if(!start) return {center:[],nearest:[]} ; // no results.
var queue = [start];
var checked = [];
var results = [];
for(i = 0; i < queue.length; i++) {
checked.push(queue[i].index); // don't check cells twice
var edges = voronoi.cells[queue[i].index].halfedges;
// use edges to find neighbors
var neighbors = edges.map(function(e) {
if(voronoi.edges[e].left == queue[i]) return voronoi.edges[e].right;
else return voronoi.edges[e].left;
})
// for each neighbor, see if its point is within the radius:
neighbors.forEach(function(n) {
if (n && checked.indexOf(n.index) == -1) {
var dx = n[0] - x;
var dy = n[1] - y;
var d = Math.sqrt(dx*dx+dy*dy);
if(d>r) checked.push(n.index) // don't check cells twice
else {
queue.push(n); // add to queue
results.push(n); // add to results
}
}
})
}
// center: the point/cell that is closest/overlapping, and within the specified radius, of point x,y
// nearest: all other cells within the specified radius of point x,y
return {center:start,nearest:results};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
I've been keen on developing a choropleth map for Namibia.But found two interesting tools. leaflet and D3, though leaflet has clear instructions to implement which i did Its not so functionally in line with what i want to do. And that is where D3Geo came in. I've everything set but this function below to set my projection.
var projection = d3.geo.conicConformal()
.rotate([, 0])
.center([0, 0])
.parallels([ , ])
.scale(1000)
Is there just no function to just simply add the co-ordinates as how its done in the leaflet function below. for us who are not so geocentric.
var map = L.map('mapid').setView([-22.26,16.52], 5);
And if there isn't, can someone please guide me on how to convert the coordinates (-22.26,16.52 )to show Namibia using the d3.geo.conicConformal().
Correct me if it didn't address your issue (maybe you can provide a minimal example showing where you are stuck, using JSFiddle for example), but if I understand well you want to move/zoom/center the displayed image on the extend of your country. Here is an example doing this (I also added some code on how the layer was added for consistency):
// Define the projection you want to use,
// setting scale and translate to some starting values :
var projection = d3.geoConicConformal()
.translate([0, 0])
.scale(1)
var layer_name = "your_layer_name";
var geo_features = topojson.feature(topoObj, topoObj.objects[layer_name]).features;
// Define the path generator :
var path = d3.geoPath().projection(projection);
var width = 800,
height = 600;
// This is the main svg object on which you are drawing :
var map = d3.select("body").append("div")
.style("width", width + "px")
.style("height", height + "px")
.append("svg")
.attr("id", "svg_map")
.attr("width", width)
.attr("height", height);
// Add you layer to the map
map.append("g").attr("id", layer_name)
.attr("class", "layers")
.selectAll("path")
.data(geo_features)
.enter().append("path")
.attr("d", path)
.attr("id", (d,i)=> "feature_" + i)
.styles({"stroke": "rgb(0, 0, 0)", "fill": "beige")
// Where the job is done :
scale_to_layer(layer_name)
function scale_to_layer(name){
var bbox_layer = undefined;
// use all the paths of the layer (if there is many features)
// to compute the layer bbox :
map.select("#"+name).selectAll('path').each(function(d, i){
var bbox_path = path.bounds(d);
if(bbox_layer === undefined){
bbox_layer = bbox_path;
}
else {
bbox_layer[0][0] = bbox_path[0][0] < bbox_layer[0][0]
? bbox_path[0][0] : bbox_layer[0][0];
bbox_layer[0][1] = bbox_path[0][1] < bbox_layer[0][1]
? bbox_path[0][1] : bbox_layer[0][1];
bbox_layer[1][0] = bbox_path[1][0] > bbox_layer[1][0]
? bbox_path[1][0] : bbox_layer[1][0];
bbox_layer[1][1] = bbox_path[1][1] > bbox_layer[1][1]
? bbox_path[1][1] : bbox_layer[1][1];
}
});
// Compute the new scale param, with a little space (5%) around the outer border :
var s = 0.95 / Math.max((bbox_layer[1][0] - bbox_layer[0][0]) / width,
(bbox_layer[1][1] - bbox_layer[0][1]) / height);
// Compute the according translation :
var t = [(width - s * (bbox_layer[1][0] + bbox_layer[0][0])) / 2,
(height - s * (bbox_layer[1][1] + bbox_layer[0][1])) / 2];
// Apply the new projections parameters :
projection.scale(s)
.translate(t);
// And redraw your paths :
map.selectAll("g.layer").selectAll("path").attr("d", path);
};
Also, note that this example use d3 v4 (but in this case it doesn't change a lot apart from the naming of geoPath and geoConicConformal)
On a dc.geoChoroplethChart, I'm setting the radius of geojson points using the pointRadius method of the path:
.geoPath().pointRadius(function(feature, index) {
var v = placeGroup.all().filter(function(item) { return item.key === feature.id; })[0].value;
return (v == 0)? 0 : pointScale(v);
});
I'm finding that it works well, but on redraw() the sizes of the points are not adjusted. They are adjusted on a render(). How do I get them to be adjusted with a redraw() as well?
Here's the full chunk of code for the geo chart, in case it's relevant
var projection = d3.geo.mercator()
.scale(1)
.translate([0, 0]);
var path = d3.geo.path()
.projection(projection);
var width = 280,
height = 200,
b = path.bounds(places), // [[left, top], [right, bottom]]
x_extent = Math.abs(b[1][0] - b[0][0]),
y_extent = Math.abs(b[1][1] - b[0][1]),
s = (.95 / Math.max(x_extent / width, y_extent / height)),
t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];
// Update projection with our actual data
projection
.scale(s)
.translate(t)
;
var mapchart = dc.geoChoroplethChart("#map-chart");
var valueDomain = [0, placeGroup.top(1)[0].value];
var maxPointRadius = Math.min(width, height) / 40,
minPointRadius = maxPointRadius / 2;
var pointScale = d3.scale.linear()
.domain(valueDomain)
.range([minPointRadius, maxPointRadius]);
mapchart.width(width)
.height(height)
.projection(projection)
.dimension(placeDim)
.group(placeGroup)
.colors(d3.scale.quantize().range(['#feb24c','#fd8d3c','#fc4e2a','#e31a1c','#bd0026'])) //first three '#ffffcc','#ffeda0', '#fed976', last one,'#800026'
.colorDomain(valueDomain)
.colorCalculator(function (d) { return d ? mapchart.colors()(d) : '#ccc'; })
.overlayGeoJson(places.features, "placeLayer", function (d) {
return d.id;
}).geoPath().pointRadius(function(feature, index) {
var v = placeGroup.all().filter(function(item) { return item.key === feature.id; })[0].value;
return (v == 0)? 0 : pointScale(v);
});
It looks like the geoChoroplethChart won't redraw the geojson unless the projection has changed. (It isn't expecting you to change the geoPath - as stated in the documentation, that's mostly a convenience method for reading and determining the center.)
https://github.com/dc-js/dc.js/blob/develop/src/geo-choropleth-chart.js#L169-L171
As a workaround, I'd suggest forcing a redraw by resetting the projection each time the chart redraws. Something like:
mapchart.on('preRedraw', function() {
mapchart.projection(projection);
});
https://github.com/dc-js/dc.js/blob/develop/web/docs/api-latest.md#basemixinon--basemixin