src/app/shared/barchart.component.ts
encapsulation | ViewEncapsulation.None |
selector | app-barchart |
styleUrls | barchart.component.css |
templateUrl | barchart.component.html |
data
|
Type: |
selected
|
Type: |
constructor()
|
createChart |
createChart()
|
created the empty chart and prepares dimensions and axes
Returns:
void
|
Private calcDOIdescendants |
calcDOIdescendants(persons: PersonData[], selected: PersonData)
|
a simple degree of interest calculation to primarily show descendants
Parameters :
Returns:
void
all persons with their DOI value filled in |
getFullName |
getFullName(p: PersonData)
|
generates the full name of a person
Parameters :
Returns:
void
the complete name |
addChildBlocks |
addChildBlocks(copy: LocationBlock[], current: LocationBlock)
|
Adds child blocks of the current block to a list of blocks and returns them.
Parameters :
Returns:
void
a list of child block and descendent blocks |
calcBaseLines |
calcBaseLines(persons: PersonData[])
|
Calculates the height at which to draw the relevant persons.
Parameters :
Returns:
void
|
getPlainCoords |
getPlainCoords(parsedLine: GraphPoint[])
|
Extracts raw coordinates from an Array of GraphPoint s
Parameters :
Returns:
void
Array of Coordinates as [Number, Bumber] |
updateChart |
updateChart()
|
Called each time input changes.
Returns:
void
|
Private chart |
chart: |
Private chartContainer |
chartContainer: |
Private colors |
colors: |
Private height |
height: |
Private lineWidth |
lineWidth: |
Default value: 20
|
Private margin |
margin: |
Private width |
width: |
Private xAxis |
xAxis: |
Private xScale |
xScale: |
Private yAxis |
yAxis: |
Private yScale |
yScale: |
import { Component, OnInit, OnChanges, ViewChild, ElementRef, Input, ViewEncapsulation } from '@angular/core';
import * as d3 from 'd3';
import {
GraphPoint,
LocationBlock,
PersonData, RelationShipData, relationShipTypeEnum, TimeNetData,
TimeNetDataService
} from '../home/timenet.service';
@Component({
selector: 'app-barchart',
templateUrl: './barchart.component.html',
styleUrls: ['./barchart.component.css'],
encapsulation: ViewEncapsulation.None
})
/**
* This component draws the chart and does some necessary calculations (e.g. degree of interest)
*/
export class BarchartComponent implements OnInit, OnChanges {
@ViewChild('chart') private chartContainer: ElementRef;
@Input() private data: TimeNetData;
private margin: any = { top: 20, bottom: 20, left: 20, right: 20};
private chart: any;
private width: number;
private height: number;
private xScale: any;
private yScale: any;
private colors: any;
private xAxis: any;
private yAxis: any;
@Input() private selected: PersonData;
private lineWidth = 20;
constructor ( ) { }
/**
* called when first switiching to this component
*/
ngOnInit() {
this.createChart();
if (this.data) {
this.updateChart();
}
}
/**
* called on input change
*/
ngOnChanges() {
if (this.chart) {
this.updateChart();
}
}
/**
* created the empty chart and prepares dimensions and axes
*/
createChart() {
let element = this.chartContainer.nativeElement;
this.width = element.offsetWidth - this.margin.left - this.margin.right;
this.height = element.offsetHeight - this.margin.top - this.margin.bottom;
let svg = d3.select(element).append('svg')
.attr('width', element.offsetWidth)
.attr('height', element.offsetHeight);
// chart plot area
this.chart = svg.append('g')
.attr('transform', `translate(${this.margin.left}, ${this.margin.top})`);
// define domains
let xDomain = [1850, 2050];
// create scale
this.xScale = d3.scaleLinear().domain(xDomain).range([0, this.width]);
// axis
this.xAxis = svg.append('g')
.attr('class', 'axis axis-x')
.attr('transform', `translate(${this.margin.left}, ${this.margin.top + this.height})`)
.call(d3.axisBottom(this.xScale));
}
/**
* a simple degree of interest calculation to primarily show descendants
* @param persons all available persons
* @param selected the currently selected person
* @returns {PersonData[]} all persons with their DOI value filled in
*/
private calcDOIdescendants(persons: PersonData[], selected: PersonData) {
let currentDOI = selected.doi;
for (let curRel of selected.relevantRelationships) {
if (curRel.relationShipType === relationShipTypeEnum['Spouse-Of']) {
if (curRel.person1ID === selected.id) {
let person2: PersonData = this.data.persons.find((p) => p.id === curRel.person2ID);
let newDOI = currentDOI * 0.1;
if (newDOI < 0.1) newDOI = 0.1;
if (person2.doi < newDOI) {
person2.doi = newDOI;
if (person2.doi > 0.1)
persons = this.calcDOIdescendants(persons, person2);
}
}
} else if (curRel.relationShipType === relationShipTypeEnum['Child-Of']) {
if (curRel.person2ID === selected.id) {
let person1: PersonData = this.data.persons.find((p) => p.id === curRel.person1ID);
if (person1.sex.startsWith('M')) {
let newDOI = currentDOI * 0.5;
// if (newDOI < 0.1) newDOI = 0.1;
if (person1.doi < newDOI) {
person1.doi = newDOI;
if (person1.doi > 0.1)
persons = this.calcDOIdescendants(persons, person1);
}
} else {
let newDOI = currentDOI * 0.5;
// if (newDOI < 0.1) newDOI = 0.1;
if (person1.doi < newDOI) {
person1.doi = newDOI;
if (person1.doi > 0.1)
persons = this.calcDOIdescendants(persons, person1);
}
}
} else if (curRel.person1ID === selected.id) {
let person2: PersonData = this.data.persons.find((p) => p.id === curRel.person2ID);
let newDOI = currentDOI * 0.1;
// if (newDOI < 0.1) newDOI = 0.1;
if (person2.doi < newDOI) {
person2.doi = newDOI;
if (person2.doi > 0.1)
persons = this.calcDOIdescendants(persons, person2);
}
}
}
}
return persons;
}
/**
* generates the full name of a person
* @param p the person who's name to generate
* @returns {string} the complete name
*/
getFullName(p: PersonData) {
let name = '';
for (var i = 0; i < p.name.length; i++) {
name += p.name[i] + ' ';
}
return name;
}
/**
* Adds child blocks of the current block to a list of blocks and returns them.
* This is called recursively to encompass all descendant blocks
* This is done for sorting these blocks in the chart.
* @param copy the still available blocks to sort
* @param current the block who's child blocks to add
* @returns {any[]} a list of child block and descendent blocks
*/
addChildBlocks(copy: LocationBlock[], current: LocationBlock) {
let blocks = [];
for (let block of current.childBlocks) {
blocks.push(block);
copy.splice(copy.indexOf(block), 1);
}
blocks.sort((a, b) => {
if (a.x < b.x) return 1;
if (a.x > b.x) return -1;
return 0;
});
let grandchildren = [];
for (let block of blocks) {
grandchildren = grandchildren.concat(this.addChildBlocks(copy, block));
}
return blocks.concat(grandchildren);
}
/**
* Calculates the height at which to draw the relevant persons.
* @param persons the relevant persons
*/
calcBaseLines(persons: PersonData[]) {
let blocks = []; // remove old blocks
for (let person of persons) {
person.block = null;
}
// group people to blocks
for (let person of persons) {
if (!person.block) {
person.block = {x: 99999, y: 0, width: 0, height: 0, persons: [person], childBlocks: []};
for (let rel of person.relevantRelationships) {
if (rel.relationShipType === relationShipTypeEnum['Spouse-Of']) {
if (rel.person1ID === person.id && persons.find((p) => p.id === rel.person2ID)) {
if (!person.block.persons.find((p) => p.id === rel.person2ID)) {
person.block.persons.push(persons.find((p) => p.id === rel.person2ID));
persons.find((p) => p.id === rel.person2ID).block = person.block;
}
} else if (rel.person2ID === person.id && persons.find((p) => p.id === rel.person1ID)) {
if (!person.block.persons.find((p) => p.id === rel.person1ID)) {
person.block.persons.push(persons.find((p) => p.id === rel.person1ID));
persons.find((p) => p.id === rel.person1ID).block = person.block;
}
}
}
}
blocks.push(person.block);
}
}
// calc block dimensions
for (let block of blocks) {
for (let person of block.persons) {
if (person.dateOfBirth < block.x) {
block.x = person.dateOfBirth;
}
if (person.dateOfDeath > block.x + block.width) {
block.width = person.dateOfDeath - block.x;
}
block.height += 2 * this.lineWidth;
}
}
// calc block children
for (let block of blocks) {
for (let person of block.persons) {
for (let rel of person.relevantRelationships) {
if (rel.relationShipType === relationShipTypeEnum['Child-Of']
&& rel.person2ID === person.id) {
let other = persons.find((p) => p.id === rel.person1ID);
if (other && !block.childBlocks.find((p) => p === other.block)) {
block.childBlocks.push(other.block);
}
}
}
}
}
// sort blocks along x with regard to children
blocks.sort((a, b) => {
if (a.x < b.x) return -1;
if (a.x > b.x) return 1;
return 0;
});
let copy = blocks.slice(1);
let first = blocks[0];
blocks = [first].concat(this.addChildBlocks(copy, first));
blocks = blocks.concat(copy);
// place blocks staggering
for (var i = 0; i < blocks.length; i++) {
let somethingChanged = true;
while (somethingChanged) {
somethingChanged = false;
for (var j = 0; j < i; j++) {
if (blocks[j].x <= blocks[i].x + blocks[i].width
&& blocks[j].x + blocks[j].width >= blocks[i].x // horizontal overlap
&& blocks[j].y < blocks[i].y + blocks[i].height
&& blocks[j].y + blocks[j].height > blocks[i].y) { // vertical overlap
blocks[i].y = blocks[j].y + blocks[j].height;
somethingChanged = true;
break;
}
}
}
}
// calc actual height of persons
for (let block of blocks) {
let currentHeight = this.lineWidth;
for (let person of block.persons) {
person.baseLine = block.y + currentHeight;
currentHeight += 2 * this.lineWidth;
}
}
/*
for (var i = 0; i < persons.length; i++) {
persons[i].baseLine = Math.random() * this.height;
}
*/
}
/**
* Extracts raw coordinates from an Array of GraphPoint s
* @param parsedLine Array of GraphPoint s
* @returns {Array} Array of Coordinates as [Number, Bumber]
*/
getPlainCoords(parsedLine: GraphPoint[]) {
let plain = [];
for (let point of parsedLine) {
plain.push([point.x, point.y]);
}
return plain;
}
/**
* Called each time input changes.
* This starts the necessary calculations and draws the chart.
*/
updateChart() {
let allPersons = this.data.persons;
// default start at random place
let index = Math.floor(Math.random() * allPersons.length);
if (!this.selected) this.selected = allPersons[index];
console.log('Starting DOI calculation');
for (var i = 0; i < allPersons.length; i++) {
allPersons[i].doi = 0.0;
}
this.selected.doi = 1.0;
let personsWithDoi = this.calcDOIdescendants(allPersons, this.selected);
console.log('Finished DOI calculation');
let relevantPersons = personsWithDoi.filter((p) => p.doi >= 0.1);
// determin timeframe
var minYear = 99999, maxYear = -99999;
for (i = 0; i < relevantPersons.length; i++) {
if (relevantPersons[i].dateOfBirth && +(relevantPersons[i].dateOfBirth) < minYear) {
minYear = +(relevantPersons[i].dateOfBirth);
}
if (relevantPersons[i].dateOfDeath && +(relevantPersons[i].dateOfDeath) > maxYear) {
maxYear = +(relevantPersons[i].dateOfDeath);
}
}
let pixelsPerYear = this.width / (maxYear - minYear);
// update scales & axis
let xDomain = [minYear, maxYear];
this.xScale.domain(xDomain);
this.xAxis.transition().call(d3.axisBottom(this.xScale));
// calc baseLine
this.calcBaseLines(relevantPersons);
// parse event data
for (i = 0; i < relevantPersons.length; i++) {
let current = relevantPersons[i];
current.parsedLine = [];
// birth
current.parsedLine.push({
x: (current.dateOfBirth - minYear) * pixelsPerYear,
y: current.baseLine});
// marriages
for (let curRel of current.relevantRelationships) {
if (curRel.relationShipType === relationShipTypeEnum['Spouse-Of']) {
let otherID: string;
if (curRel.person1ID === current.id) {
otherID = curRel.person2ID;
} else if (curRel.person2ID === current.id) {
otherID = curRel.person1ID;
}
if (!relevantPersons.find((p) => p.id === otherID)) {
break;
}
if (otherID) {
let other: PersonData = this.data.persons.find((p) => p.id === otherID);
let marriageLine = (current.baseLine + other.baseLine) / 2;
if (current.baseLine < other.baseLine) {
marriageLine -= 0.5 * this.lineWidth;
} else {
marriageLine += 0.5 * this.lineWidth;
}
if (current.dateOfBirth >= curRel.relationShipStartDate) {
current.parsedLine[0] = {
x: (curRel.relationShipStartDate - minYear) * pixelsPerYear,
y: marriageLine};
} else {
current.parsedLine.push({
x: (curRel.relationShipStartDate - minYear) * pixelsPerYear,
y: marriageLine
});
}
if (curRel.relationShipEndDate && curRel.relationShipEndDate !== current.dateOfDeath) {
current.parsedLine.push({
x: (curRel.relationShipEndDate - minYear) * pixelsPerYear,
y: current.baseLine
});
}
}
}
}
// sort marriages
current.parsedLine.sort((a, b) => {
if (a.x < b.x) return -1;
if (a.x > b.x) return 1;
return 0;
})
// add points for curves
let k = 1;
while (k < current.parsedLine.length) {
let space = current.parsedLine[k].x - current.parsedLine[k - 1].x;
if (space > 50) space = 50;
let prePoint = {
x: current.parsedLine[k].x - space,
y: current.parsedLine[k - 1].y};
current.parsedLine.splice(k, 0, prePoint);
k += 2;
}
// death
if (!(current.parsedLine[current.parsedLine.length - 1].x >= (current.dateOfDeath - minYear) * pixelsPerYear)) {
current.parsedLine.push({
x: (current.dateOfDeath - minYear) * pixelsPerYear,
y: current.parsedLine[current.parsedLine.length - 1].y
});
}
}
// prepare child-markers
let childLines = [];
for (i = 0; i < relevantPersons.length; i++) {
let current = relevantPersons[i];
let thisMarker = [];
thisMarker.push(current.parsedLine[0]);
for (let curRel of current.relevantRelationships) {
if (curRel.relationShipType === relationShipTypeEnum['Child-Of']) {
if (curRel.person1ID === current.id) {
let other: PersonData = relevantPersons.find((p) => p.id === curRel.person2ID);
if (!other) break;
if (relevantPersons.find((p) => p.id === other.id)) {
let otherHeight = other.parsedLine[0].y;
for (var k = 0; k < other.parsedLine.length; k++) {
if (other.parsedLine[k].x <= current.parsedLine[0].x) {
otherHeight = other.parsedLine[k].y;
}
}
thisMarker.push({
x: thisMarker[0].x,
y: otherHeight
});
}
}
}
}
childLines.push(thisMarker);
}
let markerPos = [];
for (var i = 0; i < childLines.length; i++) {
for (var j = 1; j < childLines[i].length; j++) {
markerPos.push(childLines[i][j]);
}
}
let lineFunction = d3.line()
.x((d) => d[0])
.y((d) => d[1])
.curve(d3.curveMonotoneX);
let update = this.chart.selectAll('.lifeline')
.data(relevantPersons);
update.exit().remove();
update
.attr('d', (d) => lineFunction(this.getPlainCoords(d.parsedLine)))
.attr('class', 'lifeline')
.attr('stroke', (d) => {return d === this.selected ? '#ffdd66' : d.sex.startsWith('F') ? '#ff5b5b' : '#6881f9'; })
.attr('stroke-width', this.lineWidth)
.attr('fill', 'none')
.on('click', (d) => {this.selected = d; this.updateChart(); } );
update
.enter()
.append('path')
.attr('d', (d) => lineFunction(this.getPlainCoords(d.parsedLine)))
.attr('class', 'lifeline')
.attr('stroke', (d) => {return d === this.selected ? '#ffdd66' : d.sex.startsWith('F') ? '#ff5b5b' : '#6881f9'; })
.attr('stroke-width', this.lineWidth)
.attr('fill', 'none')
.on('click', (d) => {this.selected = d; this.updateChart(); } );
let children = this.chart.selectAll('.childLine')
.data(childLines);
children.exit().remove();
children
.attr('d', (d) => lineFunction(this.getPlainCoords(d)))
.attr('class', 'childLine')
.attr('stroke', 'grey')
.attr('stroke-width', Math.max(this.lineWidth / 10, 2))
.attr('fill', 'none');
children
.enter()
.append('path')
.attr('d', (d) => lineFunction(this.getPlainCoords(d)))
.attr('class', 'childLine')
.attr('stroke', 'grey')
.attr('stroke-width', Math.max(this.lineWidth / 10, 2))
.attr('fill', 'none');
let marker = this.chart.selectAll('.parentMarker')
.data(markerPos);
marker.exit().remove();
marker
.attr('x', (d) => d.x - Math.max(this.lineWidth / 10, 2))
.attr('y', (d) => d.y - Math.max(this.lineWidth / 10, 2))
.attr('width', Math.max(this.lineWidth / 5, 4))
.attr('height', Math.max(this.lineWidth / 5, 4))
.attr('fill', 'black')
.attr('class', 'parentMarker');
marker
.enter()
.append('rect')
.attr('x', (d) => d.x - Math.max(this.lineWidth / 10, 2))
.attr('y', (d) => d.y - Math.max(this.lineWidth / 10, 2))
.attr('width', Math.max(this.lineWidth / 5, 4))
.attr('height', Math.max(this.lineWidth / 5, 4))
.attr('fill', 'black')
.attr('class', 'parentMarker');
let labelShadow = this.chart.selectAll('.shadow')
.data(relevantPersons);
labelShadow.exit().remove();
labelShadow
.attr('y', (d) => d.parsedLine[0].y)
.attr('x', (d) => d.parsedLine[0].x)
.attr('text-anchor', 'left')
.attr('alignment-baseline', 'central')
.attr('class', 'shadow')
.text((d) => this.getFullName(d))
.on('click', (d) => {this.selected = d; this.updateChart(); } );
labelShadow
.enter()
.append('text')
.attr('y', (d) => d.parsedLine[0].y)
.attr('x', (d) => d.parsedLine[0].x)
.attr('text-anchor', 'left')
.attr('alignment-baseline', 'central')
.attr('class', 'shadow')
.text((d) => this.getFullName(d))
.on('click', (d) => {this.selected = d; this.updateChart(); } );
let label = this.chart.selectAll('.lineLabel')
.data(relevantPersons);
label.exit().remove();
label
.attr('y', (d) => d.parsedLine[0].y)
.attr('x', (d) => d.parsedLine[0].x)
.attr('text-anchor', 'left')
.attr('alignment-baseline', 'central')
.attr('class', 'lineLabel')
.text((d) => this.getFullName(d))
.on('click', (d) => {this.selected = d; this.updateChart(); } );
label
.enter()
.append('text')
.attr('y', (d) => d.parsedLine[0].y)
.attr('x', (d) => d.parsedLine[0].x)
.attr('text-anchor', 'left')
.attr('alignment-baseline', 'central')
.attr('class', 'lineLabel')
.text((d) => this.getFullName(d))
.on('click', (d) => {this.selected = d; this.updateChart(); } );
// order back to front
this.chart.selectAll('.lifeline').raise();
this.chart.selectAll('.childLine').raise();
this.chart.selectAll('.parentMarker').raise();
this.chart.selectAll('.shadow').raise();
this.chart.selectAll('.lineLabel').raise();
}
}