How can I get pointRadius() updated on geoChoroplethChart redraw? - dc.js

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

Related

Adding a horizontal baseline to a SeriesChart in dc.js

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.)

D3 v4 Bubble Simulation in Object Constructor Not Placing Circles

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

standard coordinates in d3.js

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)

Datamaps.js access default projection and change it

I would like to use Datamaps.js in a similar way as Google Charts in order to select some regions around the world.
Firstly, I would like to access the default "setProjection" function, but I do not know how.
Then, I would like to create projections in several regions around the world.
However, I have seen only the Africa example in the Datamaps web
Any help would be appreciated.
EDIT:
What I am currently testing is this code. It surely could be improved, but it works:
function getProjection() {
//Projection por defecto
var currentProjection;
var region = $("#mapRegions")[0].value;
if (region === 0) {
// Zoom in on World
currentProjection = function(element) {
var projection = d3.geo.mercator()
.center([0, 20])
.rotate([-10, 0])
.scale(65)
.translate([element.offsetWidth / 2, element.offsetHeight / 2]);
var path = d3.geo.path()
.projection(projection);
return {
path: path,
projection: projection
};
}
} else if (region == 1) {
// Zoom in on Africa
currentProjection = function(element) {
var projection = d3.geo.mercator()
.center([23, 5])
.rotate([4.4, 0])
.scale(150)
.translate([element.offsetWidth / 2, element.offsetHeight / 2]);
var path = d3.geo.path()
.projection(projection);
return {
path: path,
projection: projection
};
}
} else if (region == 2) {
// Zoom in on Europa
currentProjection = function(element) {
var projection = d3.geo.mercator()
.center([45, 60])
.rotate([0, 0])
.scale(150)
.translate([element.offsetWidth / 2, element.offsetHeight / 2]);
var path = d3.geo.path()
.projection(projection);
return {
path: path,
projection: projection
};
}
}
return currentProjection;
}
The key was to change the d3.geo projection, as I am currently working with Mercator.

nvd3 not loading json

I've done a nvd3 graphic, but it does not load the json.
What changes do I have to do in nvd3? I'm a little lost... sorry.
nv.addGraph(function() {
var chart = nv.models.lineChart();
var fitScreen = false;
var width = 600;
var height = 400;
var zoom = 1;
chart.useInteractiveGuideline(true);
chart.xAxis
.tickFormat(d3.time.format("%Y%m%d").parse);
//.tickFormat(d3.format(',r'));
chart.yAxis
.axisLabel('Alumnos')
.tickFormat(d3.format(',.2f'));
d3.select('#chart1 svg')
.attr('perserveAspectRatio', 'xMinYMid')
.attr('width', width)
.attr('height', height)
.datum(datos);
setChartViewBox();
resizeChart();
// These resizes both do the same thing, and require recalculating the chart
//nv.utils.windowResize(chart.update);
//nv.utils.windowResize(function() { d3.select('#chart1 svg').call(chart) });
nv.utils.windowResize(resizeChart);
d3.select('#zoomIn').on('click', zoomIn);
d3.select('#zoomOut').on('click', zoomOut);
function setChartViewBox() {
var w = width * zoom,
h = height * zoom;
chart
.width(w)
.height(h);
d3.select('#chart1 svg')
.attr('viewBox', '0 0 ' + w + ' ' + h)
.transition().duration(500)
.call(chart);
}
function zoomOut() {
zoom += .25;
setChartViewBox();
}
function zoomIn() {
if (zoom <= .5) return;
zoom -= .25;
setChartViewBox();
}
// This resize simply sets the SVG's dimensions, without a need to recall the chart code
// Resizing because of the viewbox and perserveAspectRatio settings
// This scales the interior of the chart unlike the above
function resizeChart() {
var container = d3.select('#chart1');
var svg = container.select('svg');
if (fitScreen) {
// resize based on container's width AND HEIGHT
var windowSize = nv.utils.windowSize();
svg.attr("width", windowSize.width);
svg.attr("height", windowSize.height);
} else {
// resize based on container's width
var aspect = chart.width() / chart.height();
var targetWidth = parseInt(container.style('width'));
svg.attr("width", targetWidth);
svg.attr("height", Math.round(targetWidth / aspect));
}
};
return chart;
});
And the json is:
[
{
"date": "20140701",
"Baja": 0,
"No admitido": 3851,
"Pre-inscrito": 0,
"Admitido": 0,
"En reserva": 0,
"Alta": 0,
"Inactivo": 0
},
{
"date": "20140701",
"Baja": 0,
"No admitido": 3851,
"Pre-inscrito": 2468,
"Admitido": 0,
"En reserva": 0,
"Alta": 0,
"Inactivo": 0
}
]
I was looking for it but all I found is to modify the json. I did it, but it did not work. I prefer to preserve this json format if it is possible, it simplifies the process at all for me.
Thank u so much!
Mira creo que esto es lo que quieres :3
var chart = c3.generate({
data: {
json: datos,
keys: {
x : 'date', // it's possible to specify 'x' when category axis
value: ['Baja', 'No admitido', 'Pre-inscrito', 'Admitido', 'En reserva', 'Alta', 'Inactivo']
}
}
});
http://codepen.io/CristianG540/pen/bNegqV

Resources