For example https://observablehq.com/#d3/zoomable-treemap
If you paste the fragments of script into a <script> tag in a HTML file the (obviously) it doesn't work.
How are you supposed to run these things?
And what the heck is italic f?
Why are no statements terminated with a semicolon and why are things declared without var?
Surely this cannot possibly work?
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>
</head>
<body>
<h1>H1</h1?
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>
<script type="text/javascript">
chart = {
const x = d3.scaleLinear().rangeRound([0, width]);
const y = d3.scaleLinear().rangeRound([0, height]);
const svg = d3.create("svg")
.attr("viewBox", [0.5, -30.5, width, height + 30])
.style("font", "10px sans-serif");
let group = svg.append("g")
.call(render, treemap(data));
function render(group, root) {
const node = group
.selectAll("g")
.data(root.children.concat(root))
.join("g");
node.filter(d => d === root ? d.parent : d.children)
.attr("cursor", "pointer")
.on("click", d => d === root ? zoomout(root) : zoomin(d));
node.append("title")
.text(d => `${name(d)}\n${format(d.value)}`);
node.append("rect")
.attr("id", d => (d.leafUid = DOM.uid("leaf")).id)
.attr("fill", d => d === root ? "#fff" : d.children ? "#ccc" : "#ddd")
.attr("stroke", "#fff");
node.append("clipPath")
.attr("id", d => (d.clipUid = DOM.uid("clip")).id)
.append("use")
.attr("xlink:href", d => d.leafUid.href);
node.append("text")
.attr("clip-path", d => d.clipUid)
.attr("font-weight", d => d === root ? "bold" : null)
.selectAll("tspan")
.data(d => (d === root ? name(d) : d.data.name).split(/(?=[A-Z][^A-Z])/g).concat(format(d.value)))
.join("tspan")
.attr("x", 3)
.attr("y", (d, i, nodes) => `${(i === nodes.length - 1) * 0.3 + 1.1 + i * 0.9}em`)
.attr("fill-opacity", (d, i, nodes) => i === nodes.length - 1 ? 0.7 : null)
.attr("font-weight", (d, i, nodes) => i === nodes.length - 1 ? "normal" : null)
.text(d => d);
group.call(position, root);
}
function position(group, root) {
group.selectAll("g")
.attr("transform", d => d === root ? `translate(0,-30)` : `translate(${x(d.x0)},${y(d.y0)})`)
.select("rect")
.attr("width", d => d === root ? width : x(d.x1) - x(d.x0))
.attr("height", d => d === root ? 30 : y(d.y1) - y(d.y0));
}
// When zooming in, draw the new nodes on top, and fade them in.
function zoomin(d) {
const group0 = group.attr("pointer-events", "none");
const group1 = group = svg.append("g").call(render, d);
x.domain([d.x0, d.x1]);
y.domain([d.y0, d.y1]);
svg.transition()
.duration(750)
.call(t => group0.transition(t).remove()
.call(position, d.parent))
.call(t => group1.transition(t)
.attrTween("opacity", () => d3.interpolate(0, 1))
.call(position, d));
}
// When zooming out, draw the old nodes on top, and fade them out.
function zoomout(d) {
const group0 = group.attr("pointer-events", "none");
const group1 = group = svg.insert("g", "*").call(render, d.parent);
x.domain([d.parent.x0, d.parent.x1]);
y.domain([d.parent.y0, d.parent.y1]);
svg.transition()
.duration(750)
.call(t => group0.transition(t).remove()
.attrTween("opacity", () => d3.interpolate(1, 0))
.call(position, d))
.call(t => group1.transition(t)
.call(position, d.parent));
}
return svg.node();
}
data = FileAttachment("flare-2.json").json()
treemap = data => d3.treemap()
.tile(tile)
(d3.hierarchy(data)
.sum(d => d.value)
.sort((a, b) => b.value - a.value))
function tile(node, x0, y0, x1, y1) {
d3.treemapBinary(node, 0, 0, width, height);
for (const child of node.children) {
child.x0 = x0 + child.x0 / width * (x1 - x0);
child.x1 = x0 + child.x1 / width * (x1 - x0);
child.y0 = y0 + child.y0 / height * (y1 - y0);
child.y1 = y0 + child.y1 / height * (y1 - y0);
}
}
name = d => d.ancestors().reverse().map(d => d.data.name).join("/")
width = 954
height = 924
format = d3.format(",d")
d3 = require("d3#5")
</script>
</body>
</html>
If you're using Observable as a prototype tool you first have to know that they have a different flavor of Javascript. You can't just copy and paste the cells to a pure html/js file, but it's indeed very easy to use their ecosystem
Here are some links from their documentation
Observable’s not JavaScript
If you're prototyping and wants to quick embed your code
Downloading and Embedding Notebooks
Handy Embed Code Generator
Another resource on how to convert notebooks to standalone
Observable to Standalone
Here is another similar questions with answer
D3 example from Observable on my wordpress site
Hopefully that works for you
Here is how you can port your code from ObservableHQ to vanilla.js. You should be able to run this example with little to no modification.
<script src="https://d3js.org/d3.v6.min.js"></script>
<script type="text/javascript">
'use strict';
var data = {};
const width = 954;
const height = 924;
const format = d3.format(",d");
d3.json("/api/graph/flare.json").then(function (dt) {
data = dt;
chart();
})
.catch(function (error) {
console.log(error);
});
function chart(){
treemap = dt => d3.treemap()
.tile(tile)
(d3.hierarchy(dt)
.sum(d => d.value)
.sort((a, b) => b.value - a.value))
const name = d => d.ancestors().reverse().map(d => d.data.name).join("/")
const x = d3.scaleLinear().rangeRound([0, width]);
const y = d3.scaleLinear().rangeRound([0, height]);
const svg = d3.create("svg")
.attr("viewBox", [0.5, -30.5, width, height + 30])
.style("font", "10px sans-serif");
let group = svg.append("g")
.call(render, treemap(data));
}
function render(group, root) {
const node = group
.selectAll("g")
.data(root.children.concat(root))
.join("g");
node.filter(d => d === root ? d.parent : d.children)
.attr("cursor", "pointer")
.on("click", d => d === root ? zoomout(root) : zoomin(d));
node.append("title")
.text(d => `${name(d)}\n${format(d.value)}`);
node.append("rect")
.attr("id", d => (d.leafUid = d3.select("leaf")).id)
.attr("fill", d => d === root ? "#fff" : d.children ? "#ccc" : "#ddd")
.attr("stroke", "#fff");
node.append("clipPath")
.attr("id", d => (d.clipUid = d3.select("clip")).id)
.append("use")
.attr("xlink:href", d => d.leafUid.href);
node.append("text")
.attr("clip-path", d => d.clipUid)
.attr("font-weight", d => d === root ? "bold" : null)
.selectAll("tspan")
.data(d => (d === root ? name(d) : d.data.name).split(/(?=[A-Z][^A-Z])/g).concat(format(d.value)))
.join("tspan")
.attr("x", 3)
.attr("y", (d, i, nodes) => `${(i === nodes.length - 1) * 0.3 + 1.1 + i * 0.9}em`)
.attr("fill-opacity", (d, i, nodes) => i === nodes.length - 1 ? 0.7 : null)
.attr("font-weight", (d, i, nodes) => i === nodes.length - 1 ? "normal" : null)
.text(d => d);
group.call(position, root);
}
function position(group, root) {
group.selectAll("g")
.attr("transform", d => d === root ? `translate(0,-30)` : `translate(${x(d.x0)},${y(d.y0)})`)
.select("rect")
.attr("width", d => d === root ? width : x(d.x1) - x(d.x0))
.attr("height", d => d === root ? 30 : y(d.y1) - y(d.y0));
}
function zoomin(d) {
const group0 = group.attr("pointer-events", "none");
const group1 = group = svg.append("g").call(render, d);
x.domain([d.x0, d.x1]);
y.domain([d.y0, d.y1]);
svg.transition()
.duration(750)
.call(t => group0.transition(t).remove()
.call(position, d.parent))
.call(t => group1.transition(t)
.attrTween("opacity", () => d3.interpolate(0, 1))
.call(position, d));
}
function zoomout(d) {
const group0 = group.attr("pointer-events", "none");
const group1 = group = svg.insert("g", "*").call(render, d.parent);
x.domain([d.parent.x0, d.parent.x1]);
y.domain([d.parent.y0, d.parent.y1]);
svg.transition()
.duration(750)
.call(t => group0.transition(t).remove()
.attrTween("opacity", () => d3.interpolate(1, 0))
.call(position, d))
.call(t => group1.transition(t)
.call(position, d.parent));
}
return svg.node();
}
function tile(node, x0, y0, x1, y1) {
d3.treemapBinary(node, 0, 0, width, height);
for (const child of node.children) {
child.x0 = x0 + child.x0 / width * (x1 - x0);
child.x1 = x0 + child.x1 / width * (x1 - x0);
child.y0 = y0 + child.y0 / height * (y1 - y0);
child.y1 = y0 + child.y1 / height * (y1 - y0);
}
}
Related
I am replicating the Githubs contributions heatmap
I have successfully created and colorized the heatmap, but I am having problems creating the time scale that would only show the months or even better month and year. Any tips on how to display months? This is what I have so far:
let dateRange = d3.timeDay.range(new Date(2022, 0, 1), new Date()).map(e => { return { "date": e, "value": Math.floor(Math.random() * 15) } });
const cellSize = 16
const countDay = d => d.getUTCDay()
const timeWeek = d3.utcSunday;
const colorFn = d3.scaleSequential(d3.interpolateHcl( "#2be5ba", "#1d846c")).domain([
Math.floor(d3.min(dateRange, (o)=>o.value)),
Math.ceil(d3.max(dateRange, (o)=>o.value))
])
d3.select("#heatmap")
.append("svg")
.append('g')
.selectAll('rect')
.data(dateRange)
.join('rect')
.attr("width", cellSize - 1.5)
.attr("height", cellSize - 1.5)
.attr("x", (d, i) => timeWeek.count(d3.timeYear(d.date), d.date) * cellSize + 10)
.attr("y", d => countDay(d.date) * cellSize + 0.5)
.attr("fill", d => {
if(d.value == 0){
return "#e5edf3";
return "aliceblue";
}else{
return colorFn(d.value);
}})
.attr("stroke", d => {
if(d.value == 0){
return null;
return "#dbe2e8";
return "aliceblue";
}else{
return null;
}})
.attr("rx", "2")
I try to use dx/dy to add offset to group elems after drag, current code not group elements not following mouse move.
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 300);
var nodes = [
{
id:"A",
x:50,
y:50,
text:"hello"
}
]
add_box(svg,50,50,nodes)
var tooltip = d3.select('body')
.append('div')
.attr('id','tooltip')
.style('position','absolute')
.style('opacity',0)
.style('background','lightsteelblue')
function add_box(svg,x,y,nodes) {
var g = svg.selectAll('.node')
.data(nodes)
.join('g')
.attr('class','node')
g.call(d3.drag()
.on('start', dragStart)
.on('drag', dragging)
.on('end', dragEnd)
)
var txt = g.append('text')
.text(d => d.text)
.attr('x',d => d.x)
.attr('y',d => d.y)
var bbox = txt.node().getBBox()//getBoundingClientRect()
var m = 2
bbox.x -= m
bbox.y -= m
bbox.width += 2*m
bbox.height += 2*m
var rect = g.append('rect')
.attr('x',bbox.x)
.attr('y',bbox.y)
.attr('width',bbox.width)
.attr('height',bbox.height)
.attr('fill','none')
.attr('stroke','black')
}
function dragStart(event,d){
d3.select(this).raise()
.style("stroke", "")
d3.select('#tooltip')
.transition().duration(100)
.style('opacity', 1)
}
function dragging(event,d){
var x = event.x;
var y = event.y;
var dx = event.dx
var dy = event.dy
d3.select(this).select("text")
.attr("dx", dx)
.attr("dy", dy);
d3.select(this).select("rect")
.attr("dx", dx)
.attr("dy", dy);
var desc = "(" + x.toFixed(1) +"," + y.toFixed(1) + ")"
d3.select('#tooltip')
.style('left', (x+2) + 'px')
.style('top', (y-2) + 'px')
.text(desc)
}
function dragEnd(event,d){
d3.select(this)
.style("stroke", "black")
d3.select('#tooltip').style('opacity', 0)
}
<script src="https://d3js.org/d3.v7.min.js"></script>
I am following this code block:
https://observablehq.com/#d3/stacked-bar-chart
I want to make a same legend,
but I am not writing d3 in a js file,
I am using html with script,
I am wondering is there a link available to embed it in html? many thanks!
I don't think you can, ObservableHQ has it's own ecosystem for packages like this.
If you click on the name d3/color-legend, you go to this page. You can copy the code contents, but you can't seem to be able to download it, especially not in a format that can be imported with <script src="">. You can try instead to copy it into your code base with very few changes:
function legend({
color,
title,
tickSize = 6,
width = 320,
height = 44 + tickSize,
marginTop = 18,
marginRight = 0,
marginBottom = 16 + tickSize,
marginLeft = 0,
ticks = width / 64,
tickFormat,
tickValues
} = {}) {
const svg = d3.select("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.style("overflow", "visible")
.style("display", "block");
let tickAdjust = g => g.selectAll(".tick line").attr("y1", marginTop + marginBottom - height);
let x;
// Continuous
if (color.interpolate) {
const n = Math.min(color.domain().length, color.range().length);
x = color.copy().rangeRound(d3.quantize(d3.interpolate(marginLeft, width - marginRight), n));
svg.append("image")
.attr("x", marginLeft)
.attr("y", marginTop)
.attr("width", width - marginLeft - marginRight)
.attr("height", height - marginTop - marginBottom)
.attr("preserveAspectRatio", "none")
.attr("xlink:href", ramp(color.copy().domain(d3.quantize(d3.interpolate(0, 1), n))).toDataURL());
}
// Sequential
else if (color.interpolator) {
x = Object.assign(color.copy()
.interpolator(d3.interpolateRound(marginLeft, width - marginRight)), {
range() {
return [marginLeft, width - marginRight];
}
});
svg.append("image")
.attr("x", marginLeft)
.attr("y", marginTop)
.attr("width", width - marginLeft - marginRight)
.attr("height", height - marginTop - marginBottom)
.attr("preserveAspectRatio", "none")
.attr("xlink:href", ramp(color.interpolator()).toDataURL());
// scaleSequentialQuantile doesn’t implement ticks or tickFormat.
if (!x.ticks) {
if (tickValues === undefined) {
const n = Math.round(ticks + 1);
tickValues = d3.range(n).map(i => d3.quantile(color.domain(), i / (n - 1)));
}
if (typeof tickFormat !== "function") {
tickFormat = d3.format(tickFormat === undefined ? ",f" : tickFormat);
}
}
}
// Threshold
else if (color.invertExtent) {
const thresholds = color.thresholds ? color.thresholds() // scaleQuantize
:
color.quantiles ? color.quantiles() // scaleQuantile
:
color.domain(); // scaleThreshold
const thresholdFormat = tickFormat === undefined ? d => d :
typeof tickFormat === "string" ? d3.format(tickFormat) :
tickFormat;
x = d3.scaleLinear()
.domain([-1, color.range().length - 1])
.rangeRound([marginLeft, width - marginRight]);
svg.append("g")
.selectAll("rect")
.data(color.range())
.join("rect")
.attr("x", (d, i) => x(i - 1))
.attr("y", marginTop)
.attr("width", (d, i) => x(i) - x(i - 1))
.attr("height", height - marginTop - marginBottom)
.attr("fill", d => d);
tickValues = d3.range(thresholds.length);
tickFormat = i => thresholdFormat(thresholds[i], i);
}
// Ordinal
else {
x = d3.scaleBand()
.domain(color.domain())
.rangeRound([marginLeft, width - marginRight]);
svg.append("g")
.selectAll("rect")
.data(color.domain())
.join("rect")
.attr("x", x)
.attr("y", marginTop)
.attr("width", Math.max(0, x.bandwidth() - 1))
.attr("height", height - marginTop - marginBottom)
.attr("fill", color);
tickAdjust = () => {};
}
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x)
.ticks(ticks, typeof tickFormat === "string" ? tickFormat : undefined)
.tickFormat(typeof tickFormat === "function" ? tickFormat : undefined)
.tickSize(tickSize)
.tickValues(tickValues))
.call(tickAdjust)
.call(g => g.select(".domain").remove())
.call(g => g.append("text")
.attr("x", marginLeft)
.attr("y", marginTop + marginBottom - height - 6)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text(title));
return svg.node();
}
function ramp(color, n = 256) {
var canvas = document.createElement('canvas');
canvas.width = n;
canvas.height = 1;
const context = canvas.getContext("2d");
for (let i = 0; i < n; ++i) {
context.fillStyle = color(i / (n - 1));
context.fillRect(i, 0, 1, 1);
}
return canvas;
}
legend({
color: d3.scaleSequential([0, 100], d3.interpolateViridis),
title: "Temperature (°F)"
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg></svg>
My D3js sunburst chart is only displaying 2 layers of data even though there are more layers.
I want it to display all layers of data
A live version of the chart is here: D3 Chart
The code is below. please advise on how to make the chart display all of the layers of data.
I need to see more than just two layers.
export default function define(runtime, observer) {
const main = runtime.module();
main.variable(observer()).define(["md"], function(md){return(
md`# D3 Zoomable Sunburst
This variant of a [sunburst diagram](/#mbostock/d3-sunburst), a radial orientation of D3’s [hierarchical partition layout](https://github.com/d3/d3-hierarchy/blob/master/README.md#partition), shows only two layers of the [Flare visualization toolkit](https://flare.prefuse.org) package hierarchy at a time. Click a node to zoom in, or click the center to zoom out.`
)});
main.variable(observer("chart")).define("chart", ["partition","data","d3","DOM","width","color","arc","format","radius"], function(partition,data,d3,DOM,width,color,arc,format,radius)
{
const root = partition(data);
root.each(d => d.current = d);
const svg = d3.select(DOM.svg(width, width))
.style("width", "100%")
.style("height", "auto")
.style("font", "10px sans-serif");
const g = svg.append("g")
.attr("transform", `translate(${width / 2},${width / 2})`)
.on("mouseleave",mouseleave);
const path = g.append("g")
.selectAll("path")
.data(root.descendants().slice(1))
.enter().append("path")
.attr("fill", d => { while (d.depth > 1) d = d.parent; return color(d.data.name); })
.attr("fill-opacity", d => arcVisible(d.current) ? (d.children ? 0.6 : 0.4) : 0)
.attr("d", d => arc(d.current))
.on("mouseover",mouseover);
path.filter(d => d.children)
.style("cursor", "pointer")
.on("click", clicked);
path.append("title")
.text(d => `${d.ancestors().map(d => d.data.name).reverse().join("/")}\n${format(d.value)}`);
const label = g.append("g")
.attr("pointer-events", "none")
.attr("text-anchor", "middle")
.style("user-select", "none")
.selectAll("text")
.data(root.descendants().slice(1))
.enter().append("text")
.attr("dy", "0.35em")
.attr("fill-opacity", d => +labelVisible(d.current))
.attr("transform", d => labelTransform(d.current))
.text(d => d.data.name);
//percentage text
const percentage_text=svg.append("text")
.attr("id","title")
.attr("x", (width / 2))
.attr("y", (width / 2))
.attr("text-anchor", "middle")
.style("font-size", "2.5em");
const parent = g.append("circle")
.datum(root)
.attr("r", radius)
.attr("fill", "none")
.attr("pointer-events", "all")
.on("click", clicked);
function clicked(p) {
parent.datum(p.parent || root);
root.each(d => d.target = {
x0: Math.max(0, Math.min(1, (d.x0 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI,
x1: Math.max(0, Math.min(1, (d.x1 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI,
y0: Math.max(0, d.y0 - p.depth),
y1: Math.max(0, d.y1 - p.depth)
});
const t = g.transition().duration(750);
// Transition the data on all arcs, even the ones that aren’t visible,
// so that if this transition is interrupted, entering arcs will start
// the next transition from the desired position.
path.transition(t)
.tween("data", d => {
const i = d3.interpolate(d.current, d.target);
return t => d.current = i(t);
})
.filter(function(d) {
return +this.getAttribute("fill-opacity") || arcVisible(d.target);
})
.attr("fill-opacity", d => arcVisible(d.target) ? (d.children ? 0.6 : 0.4) : 0)
.attrTween("d", d => () => arc(d.current));
label.filter(function(d) {
return +this.getAttribute("fill-opacity") || labelVisible(d.target);
}).transition(t)
.attr("fill-opacity", d => +labelVisible(d.target))
.attrTween("transform", d => () => labelTransform(d.current));
}
//mouse over
const totalSize = root.descendants()[0].value;
function mouseover(d){
var percentage = (100 * d.value / totalSize).toPrecision(3);
var percentageString = percentage + "%";
if (percentage < 0.1) {
percentageString = "< 0.1%"; }
percentage_text.text(percentageString);
var sequenceArray = d.ancestors().reverse();
sequenceArray.shift(); // remove root node from the array
// Fade all the segments.
d3.selectAll("path")
.style("opacity", 0.3);
// Then highlight only those that are an ancestor of the current segment.
g.selectAll("path")
.filter(function(node) {
return (sequenceArray.indexOf(node) >= 0);
})
.style("opacity", 1);
}
//mouse leave
// Restore everything to full opacity when moving off the visualization.
function mouseleave(d) {
// Deactivate all segments during transition.
//d3.selectAll("path").on("mouseover", null);
// Transition each segment to full opacity and then reactivate it.
d3.selectAll("path")
.transition()
.duration(200)
.style("opacity", 1)
.on("end", function() {
d3.select(this).on("mouseover", mouseover);
});
percentage_text.text("");
}
function arcVisible(d) {
return d.y1 <= 3 && d.y0 >= 1 && d.x1 > d.x0;
}
function labelVisible(d) {
return d.y1 <= 3 && d.y0 >= 1 && (d.y1 - d.y0) * (d.x1 - d.x0) > 0.03;
}
function labelTransform(d) {
const x = (d.x0 + d.x1) / 2 * 180 / Math.PI;
const y = (d.y0 + d.y1) / 2 * radius;
return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`;
}
return svg.node();
}
);
main.variable(observer("data")).define("data", ["d3"], async function(d3){return(
await d3.json("data.json")
)});
main.variable(observer("partition")).define("partition", ["d3"], function(d3){return(
data => {
const root = d3.hierarchy(data)
.sum(d => d.size)
.sort((a, b) => b.value - a.value);
return d3.partition()
.size([2 * Math.PI, root.height + 1])
(root);
}
)});
main.variable(observer("color")).define("color", ["d3","data"], function(d3,data){return(
d3.scaleOrdinal().range(d3.quantize(d3.interpolateRainbow, data.children.length + 1))
)});
main.variable(observer("format")).define("format", ["d3"], function(d3){return(
d3.format(",d")
)});
main.variable(observer("width")).define("width", function(){return(
974
)});
main.variable(observer("radius")).define("radius", ["width"], function(width){return(
width / 6
)});
main.variable(observer("arc")).define("arc", ["d3","radius"], function(d3,radius){return(
d3.arc()
.startAngle(d => d.x0)
.endAngle(d => d.x1)
.padAngle(d => Math.min((d.x1 - d.x0) / 2, 0.005))
.padRadius(radius * 1.5)
.innerRadius(d => d.y0 * radius)
.outerRadius(d => Math.max(d.y0 * radius, d.y1 * radius - 1))
)});
main.variable(observer("d3")).define("d3", ["require"], function(require){return(
require("d3#5")
)});
main.variable(observer()).define(["partition","data"], function(partition,data){return(
partition(data).descendants()[1]
)});
return main;
}
I've made a couple of changes, firstly making the example more minimal by removing hover and click events.
I needed to change the arc radius and the partition method as per this example, and took text positioning logic from this fiddle.
const width = 500,
radius = width / 2,
format = d3.format(",d");
const svg = d3.select('svg')
.attr("width", width)
.attr("height", width)
.style("font", "10px sans-serif");
const g = svg.append("g")
.attr("transform", `translate(${radius},${radius})`);
const arc = d3.arc()
.startAngle(d => d.x0)
.endAngle(d => d.x1)
.padAngle(d => Math.min((d.x1 - d.x0) / 2, 0.005))
.padRadius(radius)
.innerRadius(d => Math.sqrt(d.y0))
.outerRadius(d => Math.sqrt(d.y1) - 1);
d3.json("https://gist.githubusercontent.com/MargretWG/b3f9e0a383408c6e6a45fc652e83a26c/raw/8756e2320d05a774e96983234beff81b01409315/hierarchy.json").then(data => {
const root = partition(data);
root.each(d => d.current = d);
const color = d3.scaleOrdinal().range(d3.quantize(d3.interpolateRainbow, data.children.length + 1));
const path = g.append("g")
.selectAll("path")
.data(root.descendants().slice(1))
.enter().append("path")
.attr("fill", d => {
while (d.depth > 1) d = d.parent;
return color(d.data.name);
})
.attr("fill-opacity", d => d.children ? 0.6 : 0.4)
.attr("d", d => arc(d.current));
path.append("title")
.text(d => `${d.ancestors().map(d => d.data.name).reverse().join("/")}\n${format(d.value)}`);
const label = g.append("g")
.attr("pointer-events", "none")
.attr("text-anchor", "middle")
.style("user-select", "none")
.selectAll("text")
.data(root.descendants().slice(1))
.enter().append("text")
.attr("dy", "0.35em")
.attr("transform", (d) => `translate(${arc.centroid(d)}) rotate(${getAngle(d)})`)
.text(d => d.data.name);
});
const partition = data => {
const root = d3.hierarchy(data)
.sum(d => d.size)
.sort((a, b) => b.value - a.value);
return d3.partition()
.size([2 * Math.PI, radius * radius])
(root);
}
function getAngle(d) {
// Offset the angle by 90 deg since the '0' degree axis for arc is Y axis, while
// for text it is the X axis.
var thetaDeg = (180 / Math.PI * (arc.startAngle()(d) + arc.endAngle()(d)) / 2 - 90);
// If we are rotating the text by more than 90 deg, then "flip" it.
// This is why "text-anchor", "middle" is important, otherwise, this "flip" would
// a little harder.
return (thetaDeg > 90) ? thetaDeg - 180 : thetaDeg;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
I've been implementing a force layout in d3js and I can zoom and drag the layout / nodes.
When I'm dragging a node, the behavior is fine, except that when I stopped moving, the node instantly starts moving back to it's original position, even though I haven't released the mouse click and so the event should still be considered in progress.
I tried to fix the behavior by setting d.fx and d.fy to current values, which doesn't solve anything.
Here's the code:
const WIDTH = 1600;
const HEIGHT = 900;
const V_MARGIN = 20;
const H_MARGIN = 50;
const ALPHA_DECAY = 0.03;
const VELOCITY_DECAY = 0.6;
const LINK_DISTANCE = Math.min(WIDTH, HEIGHT) / 10;
const CHARGE_FORCE = -Math.min(WIDTH, HEIGHT) / 3;
const ITERATIONS = 16;
const CIRCLE_WIDTH = 3;
const ON_HOVER_OPACITY = 0.1;
const c10 = d3.scaleOrdinal(d3.schemeCategory10);
const SVG = d3.select('body')
.append('svg')
.attr('width', WIDTH)
.attr('height', HEIGHT)
;
const g = SVG.append('g')
.attr('class', 'everything')
;
d3.json('got_social_graph.json')
.then(data => {
const nodes = data.nodes;
const links = data.links;
//Create force layout
const force = d3.forceSimulation()
.nodes(nodes)
.alphaDecay(ALPHA_DECAY)
.velocityDecay(VELOCITY_DECAY)
.force('links', d3.forceLink(links).distance(LINK_DISTANCE))
.force("collide",d3.forceCollide(d => d.influence > 15 ? d.influence : 15).iterations(ITERATIONS))
.force('charge_force', d3.forceManyBody().strength(CHARGE_FORCE))
.force('center_force', d3.forceCenter((WIDTH - H_MARGIN) / 2, (HEIGHT - V_MARGIN) / 2))
.force("y", d3.forceY(0))
.force("x", d3.forceX(0))
;
//Create links
const link = g.append('g')
.attr('class', 'link')
.selectAll('line')
.data(links)
.enter()
.append('line')
.attr('stroke-width', d => d.weight / 10)
;
//Create nodes elements
const node = g.append('g')
.attr('class', 'node')
.selectAll('circle')
.data(nodes)
.enter()
.append('g')
;
//Append circles to nodes
const circle = node.append('circle')
.attr('r', d => d.influence > 10 ? d.influence : 10)
.attr('stroke-width', CIRCLE_WIDTH)
.attr('stroke', d => c10(d.zone * 10))
.attr('fill', 'black')
;
/*const label = node.append('text')
.attr('x', 12)
.attr('y', '0.25em')
.attr('font-size', d => d.influence * 1.5 > 9 ? d.influence * 1.5 : 9)
.text(d => d.character)
;*/
//Refresh force layout data
force.on('tick', () => {
node.attr('cx', d => d.x = Math.max(H_MARGIN, Math.min(WIDTH - H_MARGIN, d.x - H_MARGIN)))
.attr('cy', d => d.y = Math.max(V_MARGIN, Math.min(HEIGHT - V_MARGIN, d.y - V_MARGIN)))
.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
link.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y)
;
});
//Handle mouse events
circle
.on('click', (d, k, n) => {
d3.select(n[k])
})
.on('mouseover', (d, k, n) => {
})
.on('mouseout', (d, k, n) => {
circle
.attr('opacity', 1)
})
;
//Handle translation events
node.call(d3.drag()
.on('start', (d, n, k) => {
if (!d3.event.active) force.alphaTarget(0.3).restart();
})
.on('drag', (d, n, k) => {
d3.select(n[k])
.attr('cx', d.x = d3.event.x)
.attr('cy', d.y = d3.event.y)
})
.on('end', (d, n, k) => {
if (!d3.event.active) force.alphaTarget(0);
}))
;
//Handle zoom events
g.call(d3.zoom()
.scaleExtent([0.8, 2.5])
.on('start', () => {
if (!d3.event.active) force.alphaTarget(0.3).restart();
d3.event.sourceEvent.stopPropagation();
})
.on('zoom', () => {
if (!d3.event.active) force.alphaTarget(0.3).restart();
g.attr('transform', d3.event.transform)
})
.on('end', () => {
if (!d3.event.active) force.alphaTarget(0);
})
);
});
I want the node to stay over my mouse cursor until the click is released. The node doesn't need to be sticky.