Last active
February 28, 2018 14:43
-
-
Save Gargamil/17babbe505218edbc8571780e2141368 to your computer and use it in GitHub Desktop.
Angular 2 zoomable Treemap Component
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
app-treemap *, | |
app-treemap *:before, | |
app-treemap *:after { | |
box-sizing: border-box; | |
} | |
app-treemap{ | |
width: 100%; | |
height: 100%; | |
margin: 0; | |
position: absolute; | |
} | |
app-treemap .feature { | |
position: relative; | |
width: calc(100% - 10px); | |
height: calc(100% - 10px); | |
margin: 5px; | |
background: #aaa; | |
border: 1px solid black; | |
} | |
app-treemap .feature .child{ | |
position: absolute; | |
width: 100%; | |
height: calc(100% - 1.5em); | |
} | |
app-treemap .feature .parent{ | |
display:block; | |
height:1.5em; | |
line-height: 1.5em; | |
font-weight: bold; | |
} | |
app-treemap .feature .parent span{ | |
cursor: pointer; | |
} | |
app-treemap .feature .parent span + span:before{ | |
content: ">"; | |
padding: 0 5px; | |
} | |
app-treemap .node { | |
position: absolute; | |
border: 1px white solid; | |
overflow: hidden; | |
-webkit-transition: opacity .8s; | |
transition: opacity .8s; | |
} | |
app-treemap .node .label { | |
display: inline; | |
font-family: sans-serif; | |
color: rgba(255, 255, 255, 1); | |
white-space: nowrap; | |
position: absolute; | |
margin: 0; | |
} | |
app-treemap .node .zoompoint{ | |
cursor: pointer; | |
} | |
app-treemap .node .labelcheckbox{ | |
vertical-align: middle; | |
} | |
app-treemap .node:hover { | |
/*opacity: 1;*/ | |
} | |
app-treemap .node:hover .label { | |
/*-webkit-filter: blur(10px); | |
filter: blur(10px);*/ | |
} | |
app-treemap .node.level-0 { | |
z-index: 4; | |
font-size: 2vmin; | |
display: none; | |
} | |
app-treemap .node.level-1 { | |
z-index: 3; | |
font-size: 1vmin; | |
} | |
app-treemap .node.level-1 .label { | |
top: 0; | |
left: 0; | |
} | |
app-treemap .node.level-2 { | |
z-index: 2; | |
font-size: 1vmin; | |
} | |
app-treemap .node.level-2 .label { | |
bottom: 0; | |
right: 0; | |
} | |
app-treemap .node.level-3 { | |
z-index: 1; | |
font-size: 1vmin; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div class="feature" id="chart"></div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Component, OnInit, ViewEncapsulation, Input } from '@angular/core'; | |
import * as d3 from 'd3'; | |
@Component({ | |
selector: 'app-treemap', | |
templateUrl: './treemap.component.html', | |
styleUrls: ['./treemap.component.css'], | |
encapsulation: ViewEncapsulation.None, | |
}) | |
export class TreemapComponent implements OnInit { | |
/* | |
BASED ON: https://gist.github.com/fallenartist/f3438a9d17e481199e04c8e7d431dbd1 | |
*/ | |
private width: number; | |
private height: number; | |
private x; | |
private y; | |
private node; | |
private root; | |
private color; | |
private maxDepth; | |
private topDepth; | |
private treemap; | |
private chart; | |
private parent; | |
constructor() { | |
this.maxDepth = 3; | |
this.topDepth = 1; | |
} | |
@Input() | |
set analyzeData(data) { | |
this.init(); | |
this.root = data; | |
this.render(data); | |
} | |
private init() { | |
this.width = 100; | |
this.height = 100; | |
this.x = d3.scaleLinear().domain([0, this.width]).range([0, this.width]); | |
this.y = d3.scaleLinear().domain([0, this.height]).range([0, this.height]); | |
this.color = d3.scaleOrdinal(d3.schemeCategory20c); | |
this.treemap = d3.treemap() | |
.size([this.width, this.height]) | |
.paddingInner(0) | |
.round(false) | |
.tile(d3.treemapSquarify) | |
this.chart = d3.select('#chart'); | |
this.parent = this.chart.append('g').attr('class', 'parent'); | |
this.chart.append('g').attr('class', 'child'); | |
} | |
private render(node) { | |
node = d3.hierarchy(node) | |
.sum((d) => { return d.size; }) | |
.sort((a, b) => { return b.value - a.value; }); | |
this.addParents(node); | |
this.treemap(node); | |
const desc = node.descendants().filter((d) => { | |
return d.depth < this.maxDepth; | |
}); | |
this.chart.select('.child').selectAll('.node').remove(); | |
const children = this.chart | |
.select('.child') | |
.selectAll('.node') | |
.data(desc) | |
.enter().append('div'); | |
children | |
.attr('class', (d) => { | |
return 'node level-' + d.depth; | |
}) | |
.attr('title', (d) => { | |
return d.data.name ? d.data.name : 'null'; | |
}) | |
.style('left', (d) => { | |
return this.x(d.x0) + '%'; | |
}) | |
.style('top', (d) => { | |
return this.y(d.y0) + '%'; | |
}) | |
.style('width', (d) => { | |
return this.x(d.x1 - d.x0) + '%'; | |
}) | |
.style('height', (d) => { | |
return this.y(d.y1 - d.y0) + '%'; | |
}) | |
.style('background-color', (d) => { | |
while (d.depth > 1) { d = d.parent; } | |
const color_ = d3.rgb(this.color(d.data.name)); | |
if(d.children) { | |
color_.opacity = 0.5; | |
} | |
return color_; | |
}); | |
const label = children.append('p') | |
.attr('class', 'label'); | |
const toplabels = label.filter((d) => { return d.depth === this.topDepth; }); | |
toplabels.append('input') | |
.attr('type', 'checkbox') | |
.attr('class', 'labelcheckbox'); | |
label.append('span') | |
.attr('class', 'labeltext') | |
.text((d) => { | |
return d.data.name ? d.data.name : 'null'; | |
}).on('mousedown touchstart', (d) => { | |
if (d.children) { | |
d.data._parent = d.parent; | |
this.render(d.data); | |
} | |
}); | |
toplabels.append('span') | |
.attr('class', 'zoompoint') | |
.text((d) => { | |
if (d.children) { | |
return ' +'; | |
} | |
}) | |
.on('mousedown touchstart', (d) => { | |
if (d.children) { | |
d.data._parent = d.parent; | |
this.render(d.data); | |
} | |
}); | |
} | |
private addParents(node) { | |
this.parent.selectAll('span').remove(); | |
const parents = []; | |
parents.push(node.data); | |
while (node.data._parent) { | |
parents.unshift(node.data._parent.data); | |
node = node.data._parent; | |
} | |
for (const p of parents){ | |
this.addParentElement(p); | |
} | |
} | |
private addParentElement(element) { | |
const newElement = this.parent.append('span'); | |
newElement.text(element.name); | |
newElement.datum(element); | |
newElement.on('mousedown touchstart', (d) => { | |
this.render(d); | |
}); | |
} | |
ngOnInit() { | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hello,
Could you please provide an example of data that will be accepted by your component? It seems different from the data expected by the source you got your inspiration from (https://gist.github.com/fallenartist/f3438a9d17e481199e04c8e7d431dbd1). For example, you have a size.
Thanks,
Pierre