I'm using https://observablehq.com/@d3/zoomable-circle-packing as an example to try d3
and circle packing
in angular. I have the data which seems to be hierarchical and I'm following along the code that's presented. However, my d3.hierarchy()
is giving me strange results. For some reason, all d.x
, d.y
and r
are all negative. I couldn't figure out why. I'm still reading some guides and books trying to understand if I can figure out how d3.hierarchy
works to understand why the values are negative. But I'm bit struggling with that. What's happening in the code and how can I show the circle packing with 3 circles for each object in the array?
I have the codes in stackblitz below.
StackBlitz
The code is here in stackblitz
Code
import { Component, OnInit, ElementRef, ViewChild, VERSION, AfterViewInit } from '@angular/core';
import * as d3 from 'd3';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit, AfterViewInit {
private diameter: number;
private margin = { top: 20, right: 20, bottom: 20, left: 20 };
private width: number;
private height: number;
private svg; any;
private g: any;
private svgContainer: ElementRef;
private color: any;
private dac: any;
private pack: any;
@ViewChild('circleContainer', { static: false }) set content(content: ElementRef) {
if (content) {
this.svgContainer = content;
}
}
ngOnInit() {
this.dac = {
"name": "DAC",
"children":
[
{
"name":
[
"Direction"
],
"children":
{
"children":
[
{"name":"leader","score":3.33,"_row":"leader"},
{"name":"same_sector","score":3.64,"_row":"same_sector"},
{"name":"senior_teams","score":3.81,"_row":"senior_teams"},
{"name":"team","score":3.81,"_row":"team"}
]
}
},
{
"title":
[
"Alignment"
],
"children":
{
"children":
[
{"name":"leader","score":3,"_row":"leader"},
{"name":"same_sector","score":3.51,"_row":"same_sector"},
{"name":"senior_teams","score":3.48,"_row":"senior_teams"},
{"name":"team","score":3.48,"_row":"team"}
]
}
},
{
"title":
[
"Commitment"
],
"children":
{
"children":
[
{"name":"leader","score":3.67,"_row":"leader"},
{"name":"same_sector","score":4.05,"_row":"same_sector"},
{"name":"senior_teams","score":3.57,"_row":"senior_teams"},
{"name":"team","score":3.57,"_row":"team"}
]
}
}
]
}
this.createChart();
}
ngAfterViewInit() {
this.width = 500 - this.margin.left - this.margin.right;
this.height = 400 - this.margin.top - this.margin.bottom;
this.color = d3.scaleLinear<string>()
.domain([1, 5])
.range(["hsl(152,80%,80%)", "hsl(228,30%,40%)"])
.interpolate(d3.interpolateHcl);
if (this.svgContainer && this.dac) {
this.createChart()
}
}
private createSVG(rect) {
this.svg = d3.select(rect)
.append('svg')
.attr('viewBox', `0 0 ${this.width / 2} ${this.height / 2}`)
.attr('preserveAspectRatio', 'xMinYMin meet')
.style('display', 'block')
.style('margin', "0 auto")
.style('background', this.color(1))
.style('cursor', 'pointer')
.attr('class', 'bubble-chart')
this.diameter = +this.svg.attr("width")
console.log("diameter: ", this.diameter);
this.pack = data => d3.pack()
.size([this.diameter - this.margin.left, this.diameter - this.margin.right])
.padding(2)
(d3.hierarchy(data)
.sum(d => d.score)
.sort((a, b) => b.value - a.value))
}
private createChart() {
let that = this;
const rect = this.svgContainer.nativeElement;
this.createSVG(rect);
const root = this.pack(this.dac);
let focus = root;
let view;
console.log(root);
this.svg = this.svg
.on('click', (event) => zoom(event, root));
const node = this.svg.append('g')
.selectAll('circle')
.data(root.descendants().slice(1))
.join('circle')
.attr('fill', d => d.children ? this.color(d.depth) : "white")
.attr('pointer-events', d => !d.children ? "none" : null)
.on('mouseover', function () { d3.select(this).attr('stroke', '#999'); })
.on('mouseout', function () { d3.select(this).attr('stroke', 'null'); })
.on('click', (event, d) => focus !== d && (zoom(event, d), event.stopPropagation()));
const label = this.svg.append('g')
.style('font', '10px Roboto')
.attr('pointer-events', 'none')
.attr('text-anchor', 'middle')
.selectAll('text')
.data(root.descendants())
.join('text')
.style('fill-opacity', d => d.parent === root ? 1 : 0)
.style('display', d => d.parent === root ? 'inline' : 'none')
.text(d => d.data.name);
zoomTo([root.x, root.y, root.r * 2]);
function zoomTo(v) {
const k = that.width / v[2];
view = v;
console.log("k: ", k);
console.log("v:",v);
label.attr('transform', d => {
console.log("dx: ", d.x)
console.log("dy: ", d.y);
return `translate(${(d.x - v[0]) * k}, ${(d.y - v[1]) * k})`});
node.attr('transform', d => `translate(${(d.x - v[0]) * k}, ${(d.y - v[1]) * k})`);
node.attr('r', d => d.r * k);
}
function zoom(event, d) {
const focus0 = focus;
focus = d;
const transition = that.svg.transition()
.duration(event.altKey ? 7500 : 750)
.tween('zoom', d => {
const i = d3.interpolateZoom(view, [focus.x, focus.y, focus.r * 2]);
return t => zoomTo(i(t));
});
label
.filter(function (d) { return d.parent === focus || this.style.display === 'inline'; })
.transition(transition)
.style('fill-opacity', d => d.parent === focus ? 1 : 0)
.on('start', function (d) { if (d.parent === focus) this.style.display = 'inline'; })
.on('end', function (d) { if (d.parent !== focus) this.style.display = 'none' });
}
}
}
You have 2 problems:
Your data structure is incorrect, you have objects as the value of your children
properties. They should be arrays.
You are setting the SVG viewBox
, not its width. Therefore, this...
this.diameter = +this.svg.attr("width")
...is just +null
, which is 0
. Because of that, the array in the size()
method will have negative values, which explains your main issue. Use your width
and height
instead.
Here is the forked code: https://stackblitz.com/edit/angular-circle-packing-uyigxs?file=src/app/app.component.ts