File

src/app/shared/barchart.component.ts

Metadata

encapsulation ViewEncapsulation.None
selector app-barchart
styleUrls barchart.component.css
templateUrl barchart.component.html

Inputs

data

Type: TimeNetData

selected

Type: PersonData

Constructor

constructor()

Methods

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 :
  • persons

    all available persons

  • selected

    the currently selected person

Returns: void

all persons with their DOI value filled in

getFullName
getFullName(p: PersonData)

generates the full name of a person

Parameters :
  • p

    the person who's name to generate

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.
This is called recursively to encompass all descendant blocks
This is done for sorting these blocks in the chart.

Parameters :
  • copy

    the still available blocks to sort

  • current

    the block who's child blocks to add

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 :
  • persons

    the relevant persons

Returns: void
getPlainCoords
getPlainCoords(parsedLine: GraphPoint[])

Extracts raw coordinates from an Array of GraphPoint s

Parameters :
  • parsedLine

    Array of GraphPoint s

Returns: void

Array of Coordinates as [Number, Bumber]

updateChart
updateChart()

Called each time input changes.
This starts the necessary calculations and draws the chart.

Returns: void

Properties

Private chart
chart: any
Private chartContainer
chartContainer: ElementRef
Private colors
colors: any
Private height
height: number
Private lineWidth
lineWidth: number
Default value: 20
Private margin
margin: any
Private width
width: number
Private xAxis
xAxis: any
Private xScale
xScale: any
Private yAxis
yAxis: any
Private yScale
yScale: any
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();
  }
}

results matching ""

    No results matching ""