Related
Please note that I'm a total beginner. I essentially don't know anything about web design, JavaScript, CSS styles and related things. This is all jargon to me, and I don't really understand how all pieces of the puzzle come together.
My goal is entirely defined in the title of this post.
It is not really a new topic: I have found similar questions here and there. However, these are pretty old, and it seems there is now in d3.js a built-in mechanism to save graphs as SVG, as described here: https://observablehq.com/#mbostock/saving-svg
For sake of example, let's start from a standalone HTML file that reproduces this graph:
https://observablehq.com/#d3/sunburst?collection=#d3/d3-hierarchy
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/#d3/sunburst
function Sunburst(data, { // data is either tabular (array of objects) or hierarchy (nested objects)
path, // as an alternative to id and parentId, returns an array identifier, imputing internal nodes
id = Array.isArray(data) ? d => d.id : null, // if tabular data, given a d in data, returns a unique identifier (string)
parentId = Array.isArray(data) ? d => d.parentId : null, // if tabular data, given a node d, returns its parent’s identifier
children, // if hierarchical data, given a d in data, returns its children
value, // given a node d, returns a quantitative value (for area encoding; null for count)
sort = (a, b) => d3.descending(a.value, b.value), // how to sort nodes prior to layout
label, // given a node d, returns the name to display on the rectangle
title, // given a node d, returns its hover text
link, // given a node d, its link (if any)
linkTarget = "_blank", // the target attribute for links (if any)
width = 640, // outer width, in pixels
height = 400, // outer height, in pixels
margin = 1, // shorthand for margins
marginTop = margin, // top margin, in pixels
marginRight = margin, // right margin, in pixels
marginBottom = margin, // bottom margin, in pixels
marginLeft = margin, // left margin, in pixels
padding = 1, // separation between arcs
radius = Math.min(width - marginLeft - marginRight, height - marginTop - marginBottom) / 2, // outer radius
color = d3.interpolateRainbow, // color scheme, if any
fill = "#ccc", // fill for arcs (if no color encoding)
fillOpacity = 0.6, // fill opacity for arcs
} = {}) {
// If id and parentId options are specified, or the path option, use d3.stratify
// to convert tabular data to a hierarchy; otherwise we assume that the data is
// specified as an object {children} with nested objects (a.k.a. the “flare.json”
// format), and use d3.hierarchy.
const root = path != null ? d3.stratify().path(path)(data)
: id != null || parentId != null ? d3.stratify().id(id).parentId(parentId)(data)
: d3.hierarchy(data, children);
// Compute the values of internal nodes by aggregating from the leaves.
value == null ? root.count() : root.sum(d => Math.max(0, value(d)));
// Sort the leaves (typically by descending value for a pleasing layout).
if (sort != null) root.sort(sort);
// Compute the partition layout. Note polar coordinates: x is angle and y is radius.
d3.partition().size([2 * Math.PI, radius])(root);
// Construct a color scale.
if (color != null) {
color = d3.scaleSequential([0, root.children.length - 1], color).unknown(fill);
root.children.forEach((child, i) => child.index = i);
}
// Construct an arc generator.
const arc = d3.arc()
.startAngle(d => d.x0)
.endAngle(d => d.x1)
.padAngle(d => Math.min((d.x1 - d.x0) / 2, 2 * padding / radius))
.padRadius(radius / 2)
.innerRadius(d => d.y0)
.outerRadius(d => d.y1 - padding);
const svg = d3.create("svg")
.attr("viewBox", [
marginRight - marginLeft - width / 2,
marginBottom - marginTop - height / 2,
width,
height
])
.attr("width", width)
.attr("height", height)
.attr("style", "max-width: 100%; height: auto; height: intrinsic;")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("text-anchor", "middle");
const cell = svg
.selectAll("a")
.data(root.descendants())
.join("a")
.attr("xlink:href", link == null ? null : d => link(d.data, d))
.attr("target", link == null ? null : linkTarget);
cell.append("path")
.attr("d", arc)
.attr("fill", color ? d => color(d.ancestors().reverse()[1]?.index) : fill)
.attr("fill-opacity", fillOpacity);
if (label != null) cell
.filter(d => (d.y0 + d.y1) / 2 * (d.x1 - d.x0) > 10)
.append("text")
.attr("transform", d => {
if (!d.depth) return;
const x = (d.x0 + d.x1) / 2 * 180 / Math.PI;
const y = (d.y0 + d.y1) / 2;
return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`;
})
.attr("dy", "0.32em")
.text(d => label(d.data, d));
if (title != null) cell.append("title")
.text(d => title(d.data, d));
return svg.node();
}
<!DOCTYPE html>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="./observablehq_sunburst.js"></script>
<script>
d3.json("https://gist.githubusercontent.com/mbostock/4348373/raw/85f18ac90409caa5529b32156aa6e71cf985263f/flare.json").then((flare) => {
chart = Sunburst(flare, {
value: d => d.size, // size of each node (file); null for internal nodes (folders)
label: d => d.name, // display name for each cell
title: (d, n) => `${n.ancestors().reverse().map(d => d.data.name).join(".")}\n${n.value.toLocaleString("en")}`, // hover text
link: (d, n) => n.children
? `https://github.com/prefuse/Flare/tree/master/flare/src/${n.ancestors().reverse().map(d => d.data.name).join("/")}`
: `https://github.com/prefuse/Flare/blob/master/flare/src/${n.ancestors().reverse().map(d => d.data.name).join("/")}.as`,
width: 1152,
height: 1152
})
document.body.appendChild(chart);
});
</script>
At this point, I really don't understand how to read https://observablehq.com/#mbostock/saving-svg
In the standalone HTML example above, what is the equivalent of "DOM.download" to obtain the same "Save as SVG" button ?
I'm newbie in D3 and I want to create a scatterplot and when I put the mouse over a point I would like to show a text near to the point, like a tooltip.
I can change the color of the point but I cannot access to the state of React Object because I've got an error:
The code where previously I set the state is this:
componentDidMount(){
let canvas = this.setCanvas();
let scales = this.setScales(this.props.data);
this.setState({
canvas: canvas,
scales: scales
}, () => {
this.setAxesToCanvas(canvas, scales);
this.setPointsToCanvas(canvas, this.props.data, scales);
});
}
The method setCanvas returns a svg:
setCanvas(){
// Add the visualization svg canvas to the container <div>
let svg = d3.select("#" + this.props.idContainer)
.append("svg")
.style("background-color", "#354560")
.style("color", "#FFFFFF") //With this we've got the color of the axis too
.attr("height", this.state.height)
.attr("width", this.state.width);
return svg;
}
And setScales returns a json object:
setScales(data){
let xRange = [this.state.margin.left, this.state.width - this.state.margin.right];
let yRange = [this.state.margin.top, this.state.height - this.state.margin.top - this.state.margin.bottom]; // flip order because y-axis origin is upper LEFT
let xScale = d3.scaleLinear()
.domain([ d3.min(data, d => parseFloat(d.value_x)) - 1, d3.max(data, d => parseFloat(d.value_x)) + 1])
.range(xRange);
let yScale = d3.scaleLinear()
.domain([ d3.max(data, d => parseFloat(d.value_y)) + 1, d3.min(data, d => parseFloat(d.value_y)) - 1])
.range(yRange);
return {"xScale" : xScale, "yScale" : yScale, "xRange" : xRange, "yRange" : yRange};
}
After setting the state I call the functions setAxesToCanvas and setPointsToCanvas. It's this last function where I have defined to catch the even "onMouseOver" how you can see:
setPointsToCanvas(canvas, data){
let xRange = [this.state.margin.left, this.state.width - this.state.margin.right];
let yRange = [this.state.margin.top, this.state.height - this.state.margin.top - this.state.margin.bottom]; // flip order because y-axis origin is upper LEFT
let xScale = d3.scaleLinear()
.domain([ d3.min(data, d => parseFloat(d.value_x)) -1, d3.max(data, d => parseFloat(d.value_x)) + 1])
.range(xRange);
let yScale = d3.scaleLinear()
.domain([ d3.max(data, d => parseFloat(d.value_y)) + 1, d3.min(data, d => parseFloat(d.value_y)) - 1])
.range(yRange);
canvas.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", 5.5) //Radius size, could map to another dimension
.attr("cx", function(d) { return xScale(parseFloat(d.value_x)); }) //x position
.attr("cy", function(d) { return yScale(parseFloat(d.value_y)); }) //y position
.style("fill", "#FFC107")
.on("mouseover", this.tipMouseOver);
}
The method that I call when the event is fire is tipMouseOver and its code is:
tipMouseOver(data, iter){
// Use D3 to select element, change color and size
d3.select(this)
.style("fill", "green")
.style("radius", "5 em");
// Specify where to put label of text
console.log("Before to set the text - data: " + data + " iter: " + iter);
this.state.canvas.append("text").attr({
id: "t" + data.value_x + "-" + data.value_y + "-" + iter, // Create an id for text so we can select it later for removing on mouseout
x: function() { return this.state.scales.xScale(data.value_x) - 30; },
y: function() { return this.state.scales.yScale(data.value_y) - 15; }
})
.text(function() {
return [data.value_x, data.value_y]; // Value of the text
});
}
How you can see in the screencap all the is executed until its arrive to the line
this.state.canvas.append("text")
If I try to pass the canvas and scales to the method which is fired when the event occurs
.on("mouseover", this.tipMouseOver(canvas, scales));
And I modify the function ...
tipMouseOver(data, iter, canvas, scales){
....
}
But when I reload the page, I've got this error:
I have checked that I cannot access to the props of the react object too.
Therefore, how can I get access to the state and props of the object to get canvas and scales?
Edit I:
I add the code in a codesandbox:
I solved what you are trying to achieve in here but not the question you asked i.e, accessing state and props inside event. But I think you will find FreeCodeCamp and component functionuseful.
Regarding the mouse events, I moved them inside setPointsToCanvas functions, so that they can have access to canvas and scales. After that it should be straight forward. Hope this helps !
Yea you need to bind functions to "this" using .bind like in React documentation. But problems I ran into were I had a build function for my D3 chart and inside this build function I had d3 directed event listeners and handlers defined inside the build function.
I noticed inside this function if I ever tried to access this.state (react state) or update react state using this.setState it would tell me it doesn't exist. I tried using arrow functions which would still cause problems in the end. I solved my problem with the proper definition of "this" by at the top of the event listener functions setting var self = this; then I could say self.state.something and also access react setState functions by saying self.setState({something: newthing})
I've build bar chart with sorting on click: https://codepen.io/wawraf/pen/gvpXWm. It's based on Mike Bostock's chart https://bl.ocks.org/mbostock/3885705.
It works fine, but when I tried to build it from scratch I realized there is something i do not fully understand: Line 72 contains following function:
var x0 = scaleX
.domain(data.sort(sort(direction))
.map(function(d) { return d[0]; }));
So it's using variable scaleX defined before (Line 16), but when instead of "scaleX" variable I want to use raw d3 reference (which is actually the same as scaleX):
var x0 = d3.scaleBand().rangeRound([0, width - margin * 2])
.domain(data.sort(sort(direction))
.map(function(d) { return d[0]; }));
axis sorting ("g" elements) doesn't work.
I would be glad if anyone could explain why it doesn't actually work.
When you do...
var x0 = scaleX.domain(data.sort(sort(direction)).map(function(d) {
return d[0];
}));
... you are not only setting a new variable x0, but changing the scaleX domain as well. As the axis is based on scaleX, not x0, it won't do the transition in your second case, which only sets x0 (without changing scaleX).
You can certainly do:
var x0 = d3.scaleBand()
.rangeRound([0, width - margin * 2])
.domain(data.sort(sort(direction))
.map(function(d) {
return d[0];
}));
As long as you change the axis' scale:
xAxis.scale(x0);
here is the updated CodePen with those changes: https://codepen.io/anon/pen/VQveBy?editors=0010
(sorry for my english bad level)
Hi I'm using D3 for the first time with mithril js. The map is ok but I have a problem with colors of provinces and it comes from the 'd' attribute to get the id of provinces.The attribute is undefined and I don't understand what is 'd' exactly. is mithril the problem? is there an other way to get 'd' attribute?
controller.map = function(el){
var width = 1160;
var height = 960;
var scale = 10000;
var offset = [width / 2, height / 2];
var center = [0, 50.64];
var rotate = [-4.668, 0];
var parallels = [51.74, 49.34];
var projection = d3.geo.albers()
.center(center)
.rotate(rotate)
.parallels(parallels)
.scale(scale)
.translate(offset)
;
var path = d3.geo.path()
.projection(projection)
;
var svg = d3.select(el).append("svg")
.attr("width",width)
.attr("height",height)
;
d3.json("belprov.json",function(error,be){
if (error) return console.error(error);
var bounds = path.bounds(topojson.feature(be, be.objects.subunits));
var hscale = scale*width / (bounds[1][0] - bounds[0][0]);
var vscale = scale*height / (bounds[1][1] - bounds[0][1]);
scale = (hscale < vscale) ? hscale : vscale;
offset = [width - (bounds[0][0] + bounds[1][0])/2,
height - (bounds[0][1] + bounds[1][1])/2];
var centroid = d3.geo.centroid(topojson.feature(be, be.objects.subunits));
center = [0, centroid[1]];
rotate = [-centroid[0],0];
projection = d3.geo.albers()
.center(center)
.rotate(rotate)
.parallels(parallels)
.scale(scale)
.translate(offset);
svg.selectAll(".province")
.data(topojson.feature(be, be.objects.provinces).features)
.enter().append("path")
.attr("class", function(d) { return "province " + d.id })
.attr("d", path)
;
})
};
The "d" attribute in a path object defines the successive coordinates of the points through which the path has to go (it also gives indication about whether the path should use bezier curves, straight lines, etc.). See some documentation here.
Be careful: in d3, d is often used as a parameter for anonymous functions representing the data currently binded to the current element. So the two are completely different things.
Here, your line
.attr("d", path)
should probably look more like
.attr("d", function(d){return d.path})
i.e., take the field path within the data elements.
You can do something like this to color diffrent paths:
//make a color scale
var color20 = d3.scale.category20();
//your code as you doing
//on making paths do
svg.selectAll(".province")
.data(topojson.feature(be, be.objects.provinces).features)
.enter().append("path")
.attr("class", function(d) { return "province " + d.id })
.style("fill", function(d){return color(d.id);})//do this to color path based on id.
.attr("d", path)
I'm working with the Protovis library to do a streamgraph of data. I want to label the different layers with the "words" array. I can't seem to get the words to line up how I'd like. I want them to be inserted where the graph is the largest for that particular layer, similar to this site:
http://mbostock.github.com/protovis/ex/jobs.html
var words = [
"tasty","delicious","yum","scrumpious","dry"];
var data = [
[23,52,6,3,16,35,24,12,35,119,2,5,65,33,81,61,55,122,3,19,2,5,65,33,81,61,55,122,3,19,54,72,85,119,23,52,6,3,16,35],
[43,2,46,78,46,25,54,72,85,119,23,52,6,3,16,35,24,12,35,119,23,52,6,3,16,35,24,12,35,119,2,5,65,33,81,61,55,122,3,19],
[2,5,65,33,81,61,55,122,3,19,54,72,85,119,23,52,6,3,16,35,2,5,65,33,81,1,5,12,95,14,12,8,84,115,15,27,6,31,6,35],
[2,5,6,3,1,6,5,12,32,191,142,22,75,139,27,32,26,13,161,35,21,52,64,35,21,61,55,123,5,142,54,58,8,11,53,2,64,3,16,35],
[2,5,65,33,81,61,55,122,3,19,54,72,85,119,23,52,6,3,16,35,2,5,65,33,81,61,55,123,5,142,54,58,8,11,53,2,64,3,16,35]];
var w = 800,
h = 300,
x = pv.Scale.linear(0, 40).range(0, w),
y = pv.Scale.linear(0, 600).range(0, h);
var vis = new pv.Panel()
.canvas('streamgraph')
.width(w)
.height(h);
vis.add(pv.Layout.Stack)
.layers(data)
.order("inside-out")
.offset("wiggle")
.x(x.by(pv.index))
.y(y)
.layer.add(pv.Area)
.fillStyle(pv.ramp("#aad", "#556").by(Math.random))
.strokeStyle(function () { this.fillStyle().alpha(.5) });
vis.render();
Try this:
vis.add(pv.Layout.Stack)
.layers(data)
.order("inside-out")
.offset("wiggle")
.x(x.by(pv.index))
.y(y)
.layer.add(pv.Area)
.fillStyle(pv.ramp("#aad", "#556").by(Math.random))
.strokeStyle(function () { this.fillStyle().alpha(.5) })
// this is new code:
.anchor("center").add(pv.Label)
.def("max", function(d) {return pv.max.index(d)})
.visible(function() {return this.index == this.max() })
.text(function(d, p) {return words[this.parent.index]});
Basically this adds a whole bunch of labels to your areas, But then only makes them visible at the index where the value is the maximum, by defining a function max on the series. I adapted this code from the code in the link you sent.