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
Related
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.)
I'm trying to build a d3js chart that zooms only on the X-axis but allows panning on both axes. The example below has the effect I desire:
https://jsfiddle.net/xpr364uo/
However, I'm having trouble translating this into my own code. For one, I'm rendering to canvas so I don't have the ability to set the "transform" attribute on some element. Also my zooming uses rescaleX/rescaleY on copies of the scales, as is the "new way" to do zooming via d3-zoom, from what I understand:
const zoomBehavior = zoom().on('zoom', () => {
const xDomain = event.transform.rescaleX(x2).domain();
const yDomain = event.transform.rescaleY(y2).domain();
xScale.domain(xDomain);
yScale.domain(yDomain);
render();
});
This works to zoom/pan on both axes. How can I modify it to get the same affect as in the fiddle? What am I supposed to do with deltaPanY (from the fiddle), in my code?
You could keep track of a second zoom transform (I'll call this yTransform) and use this to rescale the y axis. As you want the x to zoom normally, you can still use d3.event.transform.rescaleX() to rescale on the X axis, while the yTransform can be used to rescale on the Y axis.
When panning, the y translate value of yTransform should be updated with the current zoom state. Conversely, when zooming, yTransform should be used to override the change in the zoom state's y translate.
Perhaps something like:
var yTransform = d3.zoomIdentity; // initial state for the y transform
var zoom = d3.zoom()
.on("zoom", function() {
var t = d3.event.transform; // zoom state
x2 = t.rescaleX(x); // rescale x as normal (t.y is irrelevant)
// for a pan event, update the y translate
if (d3.event.sourceEvent.type != "wheel") yTransform.y = t.y;
// for a scroll, use the current y translate
else t.y = yTransform.y;
y2 = yTransform.rescaleY(y); // rescale y.
render();
})
The k and x values for yTranslate don't matter: the scale is always 1 as we aren't zooming in, and the x translate is irrelevant to rescale on the y axis. The above doesn't account for double click events, but I'll add that below.
var dots = d3.range(100)
.map(function() {
return {x: Math.random(), y: Math.random()}
})
var x = d3.scaleLinear().range([0,500])
var x2 = x.copy();
var y = d3.scaleLinear().range([0,300])
var y2 = y.copy();
var canvas = d3.select("canvas")
var context = canvas.node().getContext("2d");
// Just for reference:
var axis = d3.axisRight(y);
var g = d3.select("svg").append("g");
g.call(d3.axisRight(y2))
render();
var yTransform = d3.zoomIdentity;
var zoom = d3.zoom()
.on("zoom", function() {
var t = d3.event.transform;
x2 = t.rescaleX(x);
// For dbl clicks, d3.event.sourceEvent is null.
if (d3.event.sourceEvent && d3.event.sourceEvent.type != "wheel") yTransform.y = t.y;
else t.y = yTransform.y;
y2 = yTransform.rescaleY(y);
render();
})
canvas.call(zoom);
function render() {
context.clearRect(0,0,500,300);
dots.forEach(function(d) {
context.beginPath();
context.arc(x2(d.x), y2(d.y), 5, 0, 2 * Math.PI);
context.stroke();
})
g.call(d3.axisRight(y2));
}
canvas, svg {
position: absolute;
top: 0;
left: 0;
}
svg {
pointer-events:none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<canvas width="500" height="300"></canvas>
<svg width="500" height="300"></svg>
As I'm modifying properties directly - which is not the most ideal.
Alternatively, we could track a translate offset on the y (the difference between a y translate with dbl clicks/wheel events and without those events). Both y offset and y translate could be used to create an appropriate zoom transform:
var yOffset = 0;
var lastY = 0;
var zoom = d3.zoom()
.on("zoom", function() {
var t = d3.event.transform;
x2 = t.rescaleX(x);
// For dbl clicks, d3.event.sourceEvent is null.
if (d3.event.sourceEvent && d3.event.sourceEvent.type != "wheel") {
lastY = t.y - yOffset;
y2 = d3.zoomIdentity.translate(0,t.y-yOffset).rescaleY(y);
}
else {
yOffset = t.y - lastY; // ignore change in y for dbl click and wheel events
}
render();
})
var dots = d3.range(100)
.map(function() {
return {x: Math.random(), y: Math.random()}
})
var x = d3.scaleLinear().range([0,500])
var x2 = x.copy();
var y = d3.scaleLinear().range([0,300])
var y2 = y.copy();
var canvas = d3.select("canvas")
var context = canvas.node().getContext("2d");
// Just for reference:
var axis = d3.axisRight(y);
var g = d3.select("svg").append("g");
g.call(d3.axisRight(y2))
render();
var yOffset = 0;
var lastY = 0;
var zoom = d3.zoom()
.on("zoom", function() {
var t = d3.event.transform;
x2 = t.rescaleX(x);
// For dbl clicks, d3.event.sourceEvent is null.
if (d3.event.sourceEvent && d3.event.sourceEvent.type != "wheel") {
lastY = t.y - yOffset;
y2 = d3.zoomIdentity.translate(0,t.y-yOffset).rescaleY(y);
}
else {
yOffset = t.y - lastY; // ignore change in y for dbl click and wheel events
}
render();
})
canvas.call(zoom);
function render() {
context.clearRect(0,0,500,300);
dots.forEach(function(d) {
context.beginPath();
context.arc(x2(d.x), y2(d.y), 5, 0, 2 * Math.PI);
context.stroke();
})
g.call(d3.axisRight(y2));
}
canvas, svg {
position: absolute;
top: 0;
left: 0;
}
svg {
pointer-events:none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<canvas width="500" height="300"></canvas>
<svg width="500" height="300"></svg>
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
I'm trying to make a button-activated d3 zoom tour through three Northeastern US points, but am having a hard time getting the data to show up visually (it shows in the console, though). I'm a beginning user and can usually solve things, but this is over my head.
Here are the an example that comes close to what I'm trying to do:
Zooms between different spots in US:
http://bl.ocks.org/mbostock/6242308
The example doesn't style the data, uses TopoJSON and also uses canvas to do the zooming calls. I'm trying to do the zoom with GeoJSON(so I can link to a CartoDB table), and style it.
I've gone through a lot to make both of those things happen and am running out of successes. Right now it comes up blank and has been. I can see the data live, but can't change the styling.
What am I doing wrong here? I'm sure it's something simple, but need a nudge.
<!DOCTYPE html>
<html lang ="en">
<head>
<meta charset="utf-8">
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<style type="text/css">
canvas{
color: 'blue';
}
path.state {
color: 'red';
}
</style>
</head>
<body>
<script type="text/javascript">
var width = 960,
height = 500,
stateMap;
var sf = [-122.417, 37.775],
ny = [-74.0064, 40.7142];
var scale,
translate,
visibleArea, // minimum area threshold for points inside viewport
invisibleArea; // minimum area threshold for points outside viewport
var zoom = d3.behavior.zoom()
.size([width, height])
.on("zoom", zoomed);
var projection = d3.geo.mercator()
.translate([width/2, height/2])
.scale(500);
var canvas = d3.select("body").append("canvas")
.attr("width", width)
.attr("height", height);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var context = canvas.node().getContext("2d");
var path = d3.geo.path()
.projection(simplify)
.context(context);
stateMap = d3.json("http://linepointpath.cartodb.com/api/v2/sql?format=GeoJSON&q=SELECT * FROM GRAPHstates", function(error, stateMap) {
console.log(stateMap)
canvas
svg.selectAll("path")
.data(stateMap.feature)
.enter().append("path")
.attr("class", "state")
.attr("d", path)
.call(zoomTo(sf, 4).event)
.transition()
.duration(60 * 1000 / 89 * 2)
.each(jump);
});
var simplify = d3.geo.transform({
point: function(x, y, z) {
if (z < visibleArea) return;
x = x * scale + translate[0];
y = y * scale + translate[1];
if (x >= -10 && x <= width + 10 && y >= -10 && y <= height + 10 || z >= invisibleArea) this.stream.point(x, y);
}
});
function zoomTo(location, scale) {
var point = projection(location);
return zoom
.translate([width / 2 - point[0] * scale, height / 2 - point[1] * scale])
.scale(scale);
}
function zoomed(d) {
translate = zoom.translate();
scale = zoom.scale();
visibleArea = 1 / scale / scale;
invisibleArea = 200 * visibleArea;
context.clearRect(0, 0, width, height);
context.beginPath();
path(d);
context.stroke();
}
function jump() {
var t = d3.select(this);
(function repeat() {
t = t.transition()
.call(zoomTo(ny, 6).event)
.transition()
.call(zoomTo(sf, 4).event)
.each("end", repeat);
})();
}
</script>
</body>
</html>
I suspect the larger issue is that you're using a GeoJSON when the example you're going by is using a TopoJSON. The differences between the two are likely causing problems with how the paths are being rendered.
Another problem you're running into is that with a canvas, the function calls are different. You're using the normal svg function calls to append an SVG, bind the data, and they style it. With canvas, you interact with the elements through the canvas context object. This has a different syntax and usage than the standard SVG object you're trying to use in your code. If you follow this tutorial you'll notice she doesn't call any of the SVG functions but instead uses the context API to draw and style the canvas elements. Be sure to look at the working example of the code.
In your case, this means your code here:
svg.selectAll("path")
.data(stateMap.feature)
.enter().append("path")
.attr("class", "state")
.attr("d", path)
.call(zoomTo(sf, 4).event)
.transition()
.duration(60 * 1000 / 89 * 2)
Is unnecessary and not doing anything for you. The code that generates the path is embedded in the zoomed() function using the context object:
context.clearRect(0, 0, width, height);
context.beginPath();
path(d);
context.stroke();
It's using the context functions to create the objects you're trying to show. There are a chain of function calls that generate this behavior and you'll need to break down the chain to make sure you're getting what you want at each step.
If you want to use the GeoJSON start with just getting the map to display and then applying the zoom functionality. It'll probably make your life a lot easier in the end to iteratively build the visualization you want.
For more information on the difference between canvas and svg with D3, including examples of doing the same operation with each, checkout this blogpost and good luck with the project.