<commit_before><!DOCTYPE html>
<meta charset='utf-8'>
<script src='https://wzrd.in/standalone/parse-gedcom@latest'></script>
<script src='https://cdn.rawgit.com/jackmoore/autosize/master/dist/autosize.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.4.1/snap.svg-min.js'></script>
<script src='https://ariutta.github.io/svg-pan-zoom/dist/svg-pan-zoom.min.js'></script>
<style>
  * {
    box-sizing: border-box; 
  }
  html, body {
    font: 300 12px/1.3 arial;
    height: 100%;
    margin: 0;
  }

  .infos {
    position: absolute;
    z-index: 2;
    height: 100%;
    width: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    background: #fff;

    transition: opacity .5s ease, visibility 0s .5s linear;
  }
  .loaded .infos {
    opacity: 0;
    visibility: hidden;
  }

  aside {
    position: absolute;
    display: flex;
    flex-flow: column;
    width: 12em;
    height: 100%;
    background-color: #fff;
    border-right: 1px solid #ddd;
    padding: 1em;
  }
  aside input,
  aside textarea,
  aside select {
    width: 100%;
    border: 1px solid #ddd;
    padding: .25em;
  }
  aside label{
    padding: .5em 0;
    font-size: .8em;
  }
  aside textarea {
    resize: none;
    overflow: hidden;
  }

  main {
    display: flex;
    flex-direction: column;
    height: 100%;
    overflow: auto;
    margin-left: 12em;
  }
  svg {
    width: 100%;
    height: 100%;
  }
  @media print { 
    aside {
      display: none;
    }
    main {
      margin-left: 0;
    }
  }

  #title {
    text-align: center;
  }


  .empty-slice path {
    fill: #f5f5f5;
  }
  .SEX_f path {
    fill: #eee;
  }
  .SEX_m path {
    fill: #ccc;
  }

</style>
<script type="text/javascript">//<![CDATA[
window.onload=function(){

var tree, origin, ancestors,
    allTags,
    main = document.getElementById('main'),
    chartStyle = document.getElementById('chartStyle');

var palette = new Worker('palette.js');
palette.addEventListener('message', addPalette, false);

// file drop

window.addEventListener('dragover',function(e){
  e = e || event;
  e.preventDefault();
},false);
window.addEventListener('drop', function(e) {
  e = e || event;
  e.stopPropagation();
  e.preventDefault();
  var f = e.dataTransfer.files[0],
      reader = new FileReader();

  reader.onload = parseFileContent;
  reader.readAsText(f);
}, false);

// settings changes

var originID = document.getElementById('originID');
originID.addEventListener('change', registerOrigin);

var generations = document.getElementById('generations');

var fields = document.querySelectorAll('input');
[].forEach.call(fields, function(el) {
  el.addEventListener('change', function () { 
    updateTitle();
    updateChart();
  });
});

var titleTemplate = document.getElementById('titleTemplate');
titleTemplate.addEventListener('input', updateTitle);

var textTemplate = document.getElementById('textTemplate');
textTemplate.addEventListener('change', function () {
  updateTexts();
  updateChart();
});

var paletteSelector = document.getElementById('paletteSelector');
paletteSelector.addEventListener('change', switchPalette);

autosize(document.getElementsByTagName('textarea'));

// helpers

String.prototype.trunc =
  function( n, useWordBoundary ){
    var isTooLong = this.length > n,
        s_ = isTooLong ? this.substr(0,n-1) : this;
    s_ = (useWordBoundary && isTooLong) ? s_.substr(0,s_.lastIndexOf(' ')) : s_;
    return  isTooLong ? s_ + '...' : s_;
  };

String.prototype.truncWords =
  function( n, showFirstLetter, sep ){
    var splitted = this.split(' '),
        out = this;
    if (splitted.length > n) {
      out = splitted.slice(0, n).join(' ');
      out += showFirstLetter ? ' ' + splitted[n].substring(0, 1) : '';
      out += sep;
    }
    return out;
  };

Math.radians = function(degrees) {
  return degrees * Math.PI / 180;
};

// main

function parseFileContent (e) {
  tree = parseGedcom.parse(e.target.result);
  allTags = {};
  tree = tree.map(function (el) {
    if (el.tag !== 'INDI') return el;
    return parseIndividual(el);
  });

  createPalettes();
  registerOrigin();
  updateTitle();
  updateTexts();
  updateChart();
}

function updateChart() {

  ancestors = getAncestors(origin);

  // console.log(tree);

  drawChart(ancestors, {
    rotation: Math.radians(Number(document.getElementById('rotation').value)),
    fontSize: Number(document.getElementById('fontSize').value),
    padding: Number(document.getElementById('padding').value),
    genSpacing: Number(document.getElementById('genSpacing').value)
  });

  document.body.classList.add('loaded');

  svgPanZoom('#chart', {
    viewportSelector:'#view',
    fit:true,
    contain: true,
    center: true,
    maxZoom: 2,
    minZoom: 0.5
  });
}



function updateTexts () {

  var lines = textTemplate.value
    .split('\n')
    .filter(function (el, i) {
      return el.trim().length > 0;
    });

  tree.forEach( function(el) {
    if(el.tag !== 'INDI') return;
    el.texts = [];
    for (var i = 0; i < lines.length; i++) {
      el.texts.push(parseTextTemplate(lines[i], el));
    }
  });
}
function updateTitle () {
  document.getElementById('title').innerHTML = parseTextTemplate(titleTemplate.value, origin).join(' ');
}



function createPalettes () {
  chartStyle.innerText = '';
  paletteSelector.innerHTML = '<option value=""></option>';
  for (tag in allTags) {
    palette.postMessage({tag:tag, nbr:allTags[tag].length});
  }
}
function addPalette(e) {
  if (!addPalette.enabled) {
    // reset styles and options
    paletteSelector.disabled = '';
    addPalette.enabled = true;
  }
  // add styles
  allTags[e.data.tag].forEach( function(tagClass, i) {
    var color = 'rgba('+e.data.colors[i]._rgb.join(',')+')';
    chartStyle.innerText += '.show_'+e.data.tag+' .'+tagClass+' path{fill:'+color+'}';
  });
  // add option
  paletteSelector.innerHTML += '<option value="'+e.data.tag+'">'+e.data.tag+'</option>';
}
function switchPalette () {
  var selectedTag = paletteSelector.options[paletteSelector.selectedIndex].value;
  main.className = '';
  main.classList.add('show_'+selectedTag);
}




function registerOrigin () {
  origin = tree.filter(function (el) {
    return el.tag == 'INDI';
  })[originID.value];
}

function getAncestors (origin) {
  var individuals = [origin];
  for (var gen = 0; gen <= generations.value; gen++)
  {
    var start = Math.floor(Math.pow(2, gen-1)),
        end = Math.pow(2, gen);

    for (var i = start; i < end; i++)
    {
      var parents = getParents(individuals[i]);
      if(parents.length == 0) {
        individuals.push({});
        individuals.push({});
      } else {
        individuals.push(parents[0]);
        individuals.push(parents[1]);
      }
    }
  }
  return individuals;
}
function getParents (indiv)
{
  if(indiv == undefined || indiv.tree == undefined) return [];

  // find family where indiv is child

  var indivFamilyID = indiv.tree.find(function (el) {
     return el.tag == 'FAMC';
  });
  if(indivFamilyID == undefined) return [];

  var indivFamily = tree.find(function (el) {
     return el.pointer == indivFamilyID.data;
  });
  if(indivFamily == undefined) return [];

  // get parents IDs of that family

  var parents = [];
  var parentA = indivFamily.tree.find(function (el) {
     return el.tag == 'HUSB';
  });
  var parentB = indivFamily.tree.find(function (el) {
     return el.tag == 'WIFE';
  });

  // get parents

  if(parentA !== undefined) {
    parents.push(tree.find(function (el) {
      return el.pointer == parentA.data;
    }));
  }
  if(parentB !== undefined) {
    parents.push(tree.find(function (el) {
      return el.pointer == parentB.data;
    }));
  }

  return parents.map(parseIndividual);
}

function parseIndividual (indiv) {
  indiv.classes = [];

  // generic tags
  var tag, cl,
      exclude = ['NAME', 'FAMC', 'FAMS', 'BIRT', 'DEAT', 'EVEN', 'CHAN'];
  for (var i = 0; i < indiv.tree.length; i++) {
    tag = indiv.tree[i].tag;
    if (exclude.indexOf(tag) != -1)
      continue;
    indiv[tag] = indiv.tree[i].data;
    registerTagClass(indiv, tag, indiv.tree[i].data);
  }

  // name
  var names = parseName(indiv.tree[0].data);
  indiv.LAST = names.lastname;
  registerTagClass(indiv, 'LAST', names.lastname);
  for (var i = 0; i < names.surnames.length; i++) {
    registerTagClass(indiv, 'SURN', names.surnames[i]);
  }
  indiv.SURN = names.surnames.join(' ');

  // birth / death
  var birth = getEventInfos(indiv, 'BIRT');
  indiv.BDATE = birth.date;
  if(!indiv.BDATE || isNaN(indiv.BDATE)) indiv.BYEAR = '';
  else indiv.BYEAR = birth.date.getFullYear();

  var death = getEventInfos(indiv, 'DEAT');
  indiv.DDATE = death.date;
  if(!indiv.DDATE || isNaN(indiv.DDATE)) indiv.DYEAR = '';
  else indiv.DYEAR = death.date.getFullYear();
  
  var places,
      placesKeys = ['COUNTR','STATE','DISTR','TOWN'];

  indiv.BPLAC = birth.plac;
  if(indiv.BPLAC) {
    places = birth.plac.split(/,\s*/, 5);
    places.reverse();
    places.forEach(function (el, i) {
      registerTagClass(indiv, 'B'+placesKeys[i], el);
    });
  }
  indiv.DPLAC = death.plac;
  if(indiv.DPLAC) {
    places = death.plac.split(',', 5);
    places.reverse();
    places.forEach(function (el) {
      registerTagClass(indiv, 'D'+placesKeys[i], el);
    });
  }

  return indiv; 
}

function registerTagClass (indiv, tag, data) {
  data = data.split(/[, ]/g)[0];
  var className = tag+cssnamify(data);
  indiv.classes.push(className);
  if (allTags[tag] === undefined)
    allTags[tag] = [];
  if (allTags[tag].indexOf(className) === -1)
    allTags[tag].push(className);
}

function getData (node, tag) {
  if(node.tree == undefined) return '';
  var result = node.tree.find(function (el) {
     return el.tag == tag; 
  });
  if(result == undefined) return '';
  return result.data;
}
function getEventInfos (indiv, tag) {
  var out = {date:null, plac:null};
  if(indiv.tree == undefined) return out;

  var event = indiv.tree.find(function (el) {
     return el.tag == tag; 
  });
  if(event == undefined) return out;

  out.date = new Date(getData(event, 'DATE')),
  out.plac = getData(event, 'PLAC');

  return out;
}
function parseName (name) {
  var nameregex = /\/(.*?)\//;
  var lastname = name.match(nameregex)[1].replace(/(\s*,\s*)+/g, ' ').trim();
  name = name.replace(nameregex, '');
  var surnames = name.trim().split(/[- ]+/);
  return {lastname: lastname || '', surnames: surnames};
}
function cssnamify(str) {
  str = str.trim().toLowerCase();
  str = str.normalize('NFD').replace(/[\u0300-\u036f]/g,""); // diactritics
  return '_' + str.replace(/[^a-z0-9]/g, function(s) {
    var c = s.charCodeAt(0);
    if (c == 32) return '-';
    return '';
  });
}



function parseTextTemplate (str, indiv)
{
  var keyreg = /([A-Z]{3,}(?:\.*\d+)?)/g;
  str = str.split(keyreg).map( function(key) {
    if(!key.match(keyreg))
      return key;

    var truncate = /([A-Z]{3,})(\.*)(\d+)/g.exec(key);
    if(truncate !== null) {
      key = truncate[1];
      var truncnbr = Number(truncate[3]);
    }
    
    if(key == 'GENS') return generations.value;

    if(!indiv.hasOwnProperty(key))
      return '';

    if(Array.isArray(indiv[key]))
      text = indiv[key].join(' ');
    else text = indiv[key];

    if(truncate !== null) {
      switch (truncate[2]) {
        case '...':
          text = text.trunc(truncnbr, false);
          break;
        case '..':
          text = text.truncWords(truncnbr, false, '...');
          break;
        case '.':
          text = text.truncWords(truncnbr, true, '.');
          break;
        default:
          text = text.truncWords(truncnbr, false, '');
          break;
      }
    }
    return ' ' + text;
  });
  return str;
}


function maxTextsSizes (indivs, start, end, options) {
  var maxLength = 0, curr;
  for (var i=start; i<end; i++) {
    if(indivs[i] == undefined || indivs[i].texts == undefined)
      continue;
    indivs[i].texts.forEach( function(el, i) {
      curr = el.join(' ').length;
      if (curr > maxLength) {
        maxLength = curr;
      }
    });
  }
  return {
    width: maxLength * (options.fontSize*.35) + options.padding,
    height: indivs[0].texts.length * options.fontSize + options.padding
  };
}

function slicePathCoordinates (start, angle, r, t, reverse) {
  var arc1 = arcCoordinates(start, angle, r, reverse),
      arc2 = arcCoordinates(start+angle, -angle, r + t, reverse);
  return reverse ? { arc1:arc2, arc2:arc1 } : {arc1:arc1, arc2:arc2};
}
function arcCoordinates (start, angle, r, reverse) {
  if(reverse) {
    start = start+angle;
    angle *= -1;
  }
  return {
    r : r,
    a : {
      x : Math.cos(start) * r,
      y : Math.sin(start) * r
    },
    b : {
      x : Math.cos(start+angle) * r,
      y : Math.sin(start+angle) * r
    }
  };
}
function arcPath (start, angle, r, reverse) {
  var c = arcCoordinates(start, angle, r, reverse),
      arcdir = reverse ? '0,0' : '0,1';
  return " M"+c.a.x+","+c.a.y + " A"+r+","+r+" 0 "+arcdir+" "+c.b.x+","+c.b.y;
}
function setDefault (obj, def) {
  for (key in obj) {
    if(obj[key] === undefined && def[key] !== undefined)
      obj[key] = def[key];
  }
  return obj;
}

function drawChart (individuals, options) {

  options = setDefault(options, {
    rotation: Math.radians(90),
    genSpacing: 1,
    padding: 10,
    fontSize: 7
  });

  var radius = 30,  // center radius
      thickness,
      snap, view, fan, slice, // snapsvg objects
      coord, slicePath,
      lineCoord, linePath, lineNbr, spliceWidth,
      textAttrs = {
        fontSize: options.fontSize,
        textAnchor: 'middle'
      };

  snap = Snap('#chart');
  view = snap.select('#viewport');
  fan = snap.select('#fan');

  if(fan !== null)
    fan.remove();
  fan = view.g().attr({id: 'fan'});

  var id = 0;
  for(var gen=1; gen<=generations.value; gen++)
  {
    var isHorizontal = gen < 4;
    var arcsNbr = Math.pow(2, gen);
    var arcAngle = 2*Math.PI / arcsNbr;
    var maxTexts = maxTextsSizes(individuals, id, id+arcsNbr, options);

    thickness = isHorizontal ? maxTexts.height : maxTexts.width;

    for(var sliceID=0; sliceID<arcsNbr; sliceID++)
    {
      var startAngle = arcAngle * sliceID + options.rotation;
      var middleAngle = startAngle + arcAngle * .5;
      var isTopHalf = middleAngle >= Math.PI && middleAngle <= Math.PI*2;
      var isLeftHalf = middleAngle >= Math.PI/2 && middleAngle < Math.PI * 1.5;
      var reverse = (isHorizontal && isTopHalf) || (!isHorizontal && isLeftHalf);

      // slice

      coord = slicePathCoordinates(startAngle, arcAngle, radius, thickness);

      slicePath  = " M"+coord.arc1.a.x+","+coord.arc1.a.y;
      slicePath += " A"+coord.arc1.r+","+coord.arc1.r+" 0 0,1 "+coord.arc1.b.x+","+coord.arc1.b.y;
      slicePath += " L"+coord.arc2.a.x+","+coord.arc2.a.y;
      slicePath += " A"+coord.arc2.r+","+coord.arc2.r+" 0 0,0 "+coord.arc2.b.x+","+coord.arc2.b.y;
      slicePath += " Z";

      slice = fan.g();
      slice.addClass('slice GEN_'+gen);
      slice.path(slicePath);

      // related individual
      id++;
      var indiv = individuals[id];
      if(indiv == undefined || indiv.tree == undefined) {
        slice.addClass('empty-slice');
        continue;
      }
      slice.addClass(indiv.classes.join(' '));

      // texts

      if (isHorizontal) {
        // centered horizontal arc
        textAttrs.textpath  = arcPath(startAngle, arcAngle, radius + thickness * .5, !reverse);
        // all lines
        lineNbr = indiv.texts.length;
      } else {
        // centered vertical line
        lineCoord = slicePathCoordinates(startAngle+arcAngle*.5, 0, radius, thickness, reverse);
        textAttrs.textpath = "M"+lineCoord.arc1.a.x+","+lineCoord.arc1.a.y+" "+lineCoord.arc2.a.x+","+lineCoord.arc2.a.y;
        // limit line number to disponible slice size
        lineNbr = 0;
        spliceWidth = Math.pow(coord.arc1.a.x - coord.arc1.b.x, 2) + Math.pow(coord.arc1.a.y - coord.arc1.b.y, 2);
        indiv.texts.forEach( function(text, index) {
          if (Math.pow((index+1) * options.fontSize, 2) < spliceWidth)
            lineNbr++;
        });
      }

      indiv.texts.forEach( function(text, index) {
        if(index+1 > lineNbr) return;
        textAttrs.dy = (index+1) * options.fontSize - lineNbr*(.5*options.fontSize);
        slice.text(0,0, text )
             .attr(textAttrs)
             .textPath.attr({startOffset: '50%'});
      });
    }
    radius += thickness + options.genSpacing;
  }
}

}//]]> 

</script>
<body>
<div class="infos">Drop a GEDCOM file</div>
<aside class="controls">
  <label>title<textarea id="titleTemplate">Ascendants fan chart of SURN LAST<br>GENS generations</textarea></label>
  <label>generations<input type="number" id="generations" min="1" max="10" value="6"></label>
  <label>origin ID<input type="number" id="originID" min="0" max="9999" value="0"></label>
  <label>content<textarea id="textTemplate">SURN.1 LAST2
BYEAR - DYEAR
OCCU</textarea></label>
  <label>rotation<input type="number" id="rotation" min="0" max="360" step="45" value="90"></label>
  <label>fontSize<input type="number" id="fontSize" min="5" max="20" step="1" value="7"></label>
  <label>padding<input type="number" id="padding" min="0" max="50" step="10" value="10"></label>
  <label>generation spacing<input type="number" id="genSpacing" min="0" max="20" step="1" value="1"></label>
  <label>color palette<select id="paletteSelector" disabled="disabled"></select></label>
</aside>
<main id="main">
  <h3 id="title"></h3>
  <style id="chartStyle"></style>
  <svg id="chart"><g id="viewport"></g></svg>
</main><commit_msg>fix places not shown on templates<commit_after><!DOCTYPE html>
<meta charset='utf-8'>
<script src='https://wzrd.in/standalone/parse-gedcom@latest'></script>
<script src='https://cdn.rawgit.com/jackmoore/autosize/master/dist/autosize.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.4.1/snap.svg-min.js'></script>
<script src='https://ariutta.github.io/svg-pan-zoom/dist/svg-pan-zoom.min.js'></script>
<style>
  * {
    box-sizing: border-box; 
  }
  html, body {
    font: 300 12px/1.3 arial;
    height: 100%;
    margin: 0;
  }

  .infos {
    position: absolute;
    z-index: 2;
    height: 100%;
    width: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    background: #fff;

    transition: opacity .5s ease, visibility 0s .5s linear;
  }
  .loaded .infos {
    opacity: 0;
    visibility: hidden;
  }

  aside {
    position: absolute;
    display: flex;
    flex-flow: column;
    width: 12em;
    height: 100%;
    background-color: #fff;
    border-right: 1px solid #ddd;
    padding: 1em;
  }
  aside input,
  aside textarea,
  aside select {
    width: 100%;
    border: 1px solid #ddd;
    padding: .25em;
  }
  aside label{
    padding: .5em 0;
    font-size: .8em;
  }
  aside textarea {
    resize: none;
    overflow: hidden;
  }

  main {
    display: flex;
    flex-direction: column;
    height: 100%;
    overflow: auto;
    margin-left: 12em;
  }
  svg {
    width: 100%;
    height: 100%;
  }
  @media print { 
    aside {
      display: none;
    }
    main {
      margin-left: 0;
    }
  }

  #title {
    text-align: center;
  }


  .empty-slice path {
    fill: #f5f5f5;
  }
  .SEX_f path {
    fill: #eee;
  }
  .SEX_m path {
    fill: #ccc;
  }

</style>
<script type="text/javascript">//<![CDATA[
window.onload=function(){

var tree, origin, ancestors,
    allTags,
    main = document.getElementById('main'),
    chartStyle = document.getElementById('chartStyle');

var palette = new Worker('palette.js');
palette.addEventListener('message', addPalette, false);

// file drop

window.addEventListener('dragover',function(e){
  e = e || event;
  e.preventDefault();
},false);
window.addEventListener('drop', function(e) {
  e = e || event;
  e.stopPropagation();
  e.preventDefault();
  var f = e.dataTransfer.files[0],
      reader = new FileReader();

  reader.onload = parseFileContent;
  reader.readAsText(f);
}, false);

// settings changes

var originID = document.getElementById('originID');
originID.addEventListener('change', registerOrigin);

var generations = document.getElementById('generations');

var fields = document.querySelectorAll('input');
[].forEach.call(fields, function(el) {
  el.addEventListener('change', function () { 
    updateTitle();
    updateChart();
  });
});

var titleTemplate = document.getElementById('titleTemplate');
titleTemplate.addEventListener('input', updateTitle);

var textTemplate = document.getElementById('textTemplate');
textTemplate.addEventListener('change', function () {
  updateTexts();
  updateChart();
});

var paletteSelector = document.getElementById('paletteSelector');
paletteSelector.addEventListener('change', switchPalette);

autosize(document.getElementsByTagName('textarea'));

// helpers

String.prototype.trunc =
  function( n, useWordBoundary ){
    var isTooLong = this.length > n,
        s_ = isTooLong ? this.substr(0,n-1) : this;
    s_ = (useWordBoundary && isTooLong) ? s_.substr(0,s_.lastIndexOf(' ')) : s_;
    return  isTooLong ? s_ + '...' : s_;
  };

String.prototype.truncWords =
  function( n, showFirstLetter, sep ){
    var splitted = this.split(' '),
        out = this;
    if (splitted.length > n) {
      out = splitted.slice(0, n).join(' ');
      out += showFirstLetter ? ' ' + splitted[n].substring(0, 1) : '';
      out += sep;
    }
    return out;
  };

Math.radians = function(degrees) {
  return degrees * Math.PI / 180;
};

// main

function parseFileContent (e) {
  tree = parseGedcom.parse(e.target.result);
  allTags = {};
  tree = tree.map(function (el) {
    if (el.tag !== 'INDI') return el;
    return parseIndividual(el);
  });

  createPalettes();
  registerOrigin();
  updateTitle();
  updateTexts();
  updateChart();
}

function updateChart() {

  ancestors = getAncestors(origin);

  // console.log(tree);

  drawChart(ancestors, {
    rotation: Math.radians(Number(document.getElementById('rotation').value)),
    fontSize: Number(document.getElementById('fontSize').value),
    padding: Number(document.getElementById('padding').value),
    genSpacing: Number(document.getElementById('genSpacing').value)
  });

  document.body.classList.add('loaded');

  svgPanZoom('#chart', {
    viewportSelector:'#view',
    fit:true,
    contain: true,
    center: true,
    maxZoom: 2,
    minZoom: 0.5
  });
}



function updateTexts () {

  var lines = textTemplate.value
    .split('\n')
    .filter(function (el, i) {
      return el.trim().length > 0;
    });

  tree.forEach( function(el) {
    if(el.tag !== 'INDI') return;
    el.texts = [];
    for (var i = 0; i < lines.length; i++) {
      el.texts.push(parseTextTemplate(lines[i], el));
    }
  });
}
function updateTitle () {
  document.getElementById('title').innerHTML = parseTextTemplate(titleTemplate.value, origin).join(' ');
}



function createPalettes () {
  chartStyle.innerText = '';
  paletteSelector.innerHTML = '<option value=""></option>';
  for (tag in allTags) {
    palette.postMessage({tag:tag, nbr:allTags[tag].length});
  }
}
function addPalette(e) {
  if (!addPalette.enabled) {
    // reset styles and options
    paletteSelector.disabled = '';
    addPalette.enabled = true;
  }
  // add styles
  allTags[e.data.tag].forEach( function(tagClass, i) {
    var color = 'rgba('+e.data.colors[i]._rgb.join(',')+')';
    chartStyle.innerText += '.show_'+e.data.tag+' .'+tagClass+' path{fill:'+color+'}';
  });
  // add option
  paletteSelector.innerHTML += '<option value="'+e.data.tag+'">'+e.data.tag+'</option>';
}
function switchPalette () {
  var selectedTag = paletteSelector.options[paletteSelector.selectedIndex].value;
  main.className = '';
  main.classList.add('show_'+selectedTag);
}




function registerOrigin () {
  origin = tree.filter(function (el) {
    return el.tag == 'INDI';
  })[originID.value];
}

function getAncestors (origin) {
  var individuals = [origin];
  for (var gen = 0; gen <= generations.value; gen++)
  {
    var start = Math.floor(Math.pow(2, gen-1)),
        end = Math.pow(2, gen);

    for (var i = start; i < end; i++)
    {
      var parents = getParents(individuals[i]);
      if(parents.length == 0) {
        individuals.push({});
        individuals.push({});
      } else {
        individuals.push(parents[0]);
        individuals.push(parents[1]);
      }
    }
  }
  return individuals;
}
function getParents (indiv)
{
  if(indiv == undefined || indiv.tree == undefined) return [];

  // find family where indiv is child

  var indivFamilyID = indiv.tree.find(function (el) {
     return el.tag == 'FAMC';
  });
  if(indivFamilyID == undefined) return [];

  var indivFamily = tree.find(function (el) {
     return el.pointer == indivFamilyID.data;
  });
  if(indivFamily == undefined) return [];

  // get parents IDs of that family

  var parents = [];
  var parentA = indivFamily.tree.find(function (el) {
     return el.tag == 'HUSB';
  });
  var parentB = indivFamily.tree.find(function (el) {
     return el.tag == 'WIFE';
  });

  // get parents

  if(parentA !== undefined) {
    parents.push(tree.find(function (el) {
      return el.pointer == parentA.data;
    }));
  }
  if(parentB !== undefined) {
    parents.push(tree.find(function (el) {
      return el.pointer == parentB.data;
    }));
  }

  return parents.map(parseIndividual);
}

function parseIndividual (indiv) {
  indiv.classes = [];

  // generic tags
  var tag, cl,
      exclude = ['NAME', 'FAMC', 'FAMS', 'BIRT', 'DEAT', 'EVEN', 'CHAN'];
  for (var i = 0; i < indiv.tree.length; i++) {
    tag = indiv.tree[i].tag;
    if (exclude.indexOf(tag) != -1)
      continue;
    indiv[tag] = indiv.tree[i].data;
    registerTagClass(indiv, tag, indiv.tree[i].data);
  }

  // name
  var names = parseName(indiv.tree[0].data);
  indiv.LAST = names.lastname;
  registerTagClass(indiv, 'LAST', names.lastname);
  for (var i = 0; i < names.surnames.length; i++) {
    registerTagClass(indiv, 'SURN', names.surnames[i]);
  }
  indiv.SURN = names.surnames.join(' ');

  // birth / death
  var birth = getEventInfos(indiv, 'BIRT');
  indiv.BDATE = birth.date;
  if(!indiv.BDATE || isNaN(indiv.BDATE)) indiv.BYEAR = '';
  else indiv.BYEAR = birth.date.getFullYear();

  var death = getEventInfos(indiv, 'DEAT');
  indiv.DDATE = death.date;
  if(!indiv.DDATE || isNaN(indiv.DDATE)) indiv.DYEAR = '';
  else indiv.DYEAR = death.date.getFullYear();
  
  var places,
      placesKeys = ['COUNTR','STATE','DISTR','TOWN'];

  indiv.BPLAC = birth.plac;
  if(indiv.BPLAC) {
    places = birth.plac.split(/,\s*/, 5);
    places.reverse();
    places.forEach(function (el, i) {
      registerTagClass(indiv, 'B'+placesKeys[i], el);
      indiv['B'+placesKeys[i]] = el;
    });
  }
  indiv.DPLAC = death.plac;
  if(indiv.DPLAC) {
    places = death.plac.split(',', 5);
    places.reverse();
    places.forEach(function (el) {
      registerTagClass(indiv, 'D'+placesKeys[i], el);
      indiv['D'+placesKeys[i]] = el;
    });
  }

  return indiv; 
}

function registerTagClass (indiv, tag, data) {
  data = data.split(/[, ]/g)[0];
  var className = tag+cssnamify(data);
  indiv.classes.push(className);
  if (allTags[tag] === undefined)
    allTags[tag] = [];
  if (allTags[tag].indexOf(className) === -1)
    allTags[tag].push(className);
}

function getData (node, tag) {
  if(node.tree == undefined) return '';
  var result = node.tree.find(function (el) {
     return el.tag == tag; 
  });
  if(result == undefined) return '';
  return result.data;
}
function getEventInfos (indiv, tag) {
  var out = {date:null, plac:null};
  if(indiv.tree == undefined) return out;

  var event = indiv.tree.find(function (el) {
     return el.tag == tag; 
  });
  if(event == undefined) return out;

  out.date = new Date(getData(event, 'DATE')),
  out.plac = getData(event, 'PLAC');

  return out;
}
function parseName (name) {
  var nameregex = /\/(.*?)\//;
  var lastname = name.match(nameregex)[1].replace(/(\s*,\s*)+/g, ' ').trim();
  name = name.replace(nameregex, '');
  var surnames = name.trim().split(/[- ]+/);
  return {lastname: lastname || '', surnames: surnames};
}
function cssnamify(str) {
  str = str.trim().toLowerCase();
  str = str.normalize('NFD').replace(/[\u0300-\u036f]/g,""); // diactritics
  return '_' + str.replace(/[^a-z0-9]/g, function(s) {
    var c = s.charCodeAt(0);
    if (c == 32) return '-';
    return '';
  });
}



function parseTextTemplate (str, indiv)
{
  var keyreg = /([A-Z]{3,}(?:\.*\d+)?)/g;
  str = str.split(keyreg).map( function(key) {
    if(!key.match(keyreg))
      return key;

    var truncate = /([A-Z]{3,})(\.*)(\d+)/g.exec(key);
    if(truncate !== null) {
      key = truncate[1];
      var truncnbr = Number(truncate[3]);
    }

    if(key == 'GENS') return generations.value;

    if(!indiv.hasOwnProperty(key))
      return '';

    if(Array.isArray(indiv[key]))
      text = indiv[key].join(' ');
    else text = indiv[key];

    if(truncate !== null) {
      switch (truncate[2]) {
        case '...':
          text = text.trunc(truncnbr, false);
          break;
        case '..':
          text = text.truncWords(truncnbr, false, '...');
          break;
        case '.':
          text = text.truncWords(truncnbr, true, '.');
          break;
        default:
          text = text.truncWords(truncnbr, false, '');
          break;
      }
    }
    return ' ' + text;
  });
  return str;
}


function maxTextsSizes (indivs, start, end, options) {
  var maxLength = 0, curr;
  for (var i=start; i<end; i++) {
    if(indivs[i] == undefined || indivs[i].texts == undefined)
      continue;
    indivs[i].texts.forEach( function(el, i) {
      curr = el.join(' ').length;
      if (curr > maxLength) {
        maxLength = curr;
      }
    });
  }
  return {
    width: maxLength * (options.fontSize*.35) + options.padding,
    height: indivs[0].texts.length * options.fontSize + options.padding
  };
}

function slicePathCoordinates (start, angle, r, t, reverse) {
  var arc1 = arcCoordinates(start, angle, r, reverse),
      arc2 = arcCoordinates(start+angle, -angle, r + t, reverse);
  return reverse ? { arc1:arc2, arc2:arc1 } : {arc1:arc1, arc2:arc2};
}
function arcCoordinates (start, angle, r, reverse) {
  if(reverse) {
    start = start+angle;
    angle *= -1;
  }
  return {
    r : r,
    a : {
      x : Math.cos(start) * r,
      y : Math.sin(start) * r
    },
    b : {
      x : Math.cos(start+angle) * r,
      y : Math.sin(start+angle) * r
    }
  };
}
function arcPath (start, angle, r, reverse) {
  var c = arcCoordinates(start, angle, r, reverse),
      arcdir = reverse ? '0,0' : '0,1';
  return " M"+c.a.x+","+c.a.y + " A"+r+","+r+" 0 "+arcdir+" "+c.b.x+","+c.b.y;
}
function setDefault (obj, def) {
  for (key in obj) {
    if(obj[key] === undefined && def[key] !== undefined)
      obj[key] = def[key];
  }
  return obj;
}

function drawChart (individuals, options) {

  options = setDefault(options, {
    rotation: Math.radians(90),
    genSpacing: 1,
    padding: 10,
    fontSize: 7
  });

  var radius = 30,  // center radius
      thickness,
      snap, view, fan, slice, // snapsvg objects
      coord, slicePath,
      lineCoord, linePath, lineNbr, spliceWidth,
      textAttrs = {
        fontSize: options.fontSize,
        textAnchor: 'middle'
      };

  snap = Snap('#chart');
  view = snap.select('#viewport');
  fan = snap.select('#fan');

  if(fan !== null)
    fan.remove();
  fan = view.g().attr({id: 'fan'});

  var id = 0;
  for(var gen=1; gen<=generations.value; gen++)
  {
    var isHorizontal = gen < 4;
    var arcsNbr = Math.pow(2, gen);
    var arcAngle = 2*Math.PI / arcsNbr;
    var maxTexts = maxTextsSizes(individuals, id, id+arcsNbr, options);

    thickness = isHorizontal ? maxTexts.height : maxTexts.width;

    for(var sliceID=0; sliceID<arcsNbr; sliceID++)
    {
      var startAngle = arcAngle * sliceID + options.rotation;
      var middleAngle = startAngle + arcAngle * .5;
      var isTopHalf = middleAngle >= Math.PI && middleAngle <= Math.PI*2;
      var isLeftHalf = middleAngle >= Math.PI/2 && middleAngle < Math.PI * 1.5;
      var reverse = (isHorizontal && isTopHalf) || (!isHorizontal && isLeftHalf);

      // slice

      coord = slicePathCoordinates(startAngle, arcAngle, radius, thickness);

      slicePath  = " M"+coord.arc1.a.x+","+coord.arc1.a.y;
      slicePath += " A"+coord.arc1.r+","+coord.arc1.r+" 0 0,1 "+coord.arc1.b.x+","+coord.arc1.b.y;
      slicePath += " L"+coord.arc2.a.x+","+coord.arc2.a.y;
      slicePath += " A"+coord.arc2.r+","+coord.arc2.r+" 0 0,0 "+coord.arc2.b.x+","+coord.arc2.b.y;
      slicePath += " Z";

      slice = fan.g();
      slice.addClass('slice GEN_'+gen);
      slice.path(slicePath);

      // related individual
      id++;
      var indiv = individuals[id];
      if(indiv == undefined || indiv.tree == undefined) {
        slice.addClass('empty-slice');
        continue;
      }
      slice.addClass(indiv.classes.join(' '));

      // texts

      if (isHorizontal) {
        // centered horizontal arc
        textAttrs.textpath  = arcPath(startAngle, arcAngle, radius + thickness * .5, !reverse);
        // all lines
        lineNbr = indiv.texts.length;
      } else {
        // centered vertical line
        lineCoord = slicePathCoordinates(startAngle+arcAngle*.5, 0, radius, thickness, reverse);
        textAttrs.textpath = "M"+lineCoord.arc1.a.x+","+lineCoord.arc1.a.y+" "+lineCoord.arc2.a.x+","+lineCoord.arc2.a.y;
        // limit line number to disponible slice size
        lineNbr = 0;
        spliceWidth = Math.pow(coord.arc1.a.x - coord.arc1.b.x, 2) + Math.pow(coord.arc1.a.y - coord.arc1.b.y, 2);
        indiv.texts.forEach( function(text, index) {
          if (Math.pow((index+1) * options.fontSize, 2) < spliceWidth)
            lineNbr++;
        });
      }

      indiv.texts.forEach( function(text, index) {
        if(index+1 > lineNbr) return;
        textAttrs.dy = (index+1) * options.fontSize - lineNbr*(.5*options.fontSize);
        slice.text(0,0, text )
             .attr(textAttrs)
             .textPath.attr({startOffset: '50%'});
      });
    }
    radius += thickness + options.genSpacing;
  }
}

}//]]> 

</script>
<body>
<div class="infos">Drop a GEDCOM file</div>
<aside class="controls">
  <label>title<textarea id="titleTemplate">Ascendants fan chart of SURN LAST<br>GENS generations</textarea></label>
  <label>generations<input type="number" id="generations" min="1" max="10" value="6"></label>
  <label>origin ID<input type="number" id="originID" min="0" max="9999" value="0"></label>
  <label>content<textarea id="textTemplate">SURN.1 LAST2
BYEAR - DYEAR
OCCU</textarea></label>
  <label>rotation<input type="number" id="rotation" min="0" max="360" step="45" value="90"></label>
  <label>fontSize<input type="number" id="fontSize" min="5" max="20" step="1" value="7"></label>
  <label>padding<input type="number" id="padding" min="0" max="50" step="10" value="10"></label>
  <label>generation spacing<input type="number" id="genSpacing" min="0" max="20" step="1" value="1"></label>
  <label>color palette<select id="paletteSelector" disabled="disabled"></select></label>
</aside>
<main id="main">
  <h3 id="title"></h3>
  <style id="chartStyle"></style>
  <svg id="chart"><g id="viewport"></g></svg>
</main>
{'fraction_non_alphanumeric': 0.1783612494341331, 'fraction_numerical': 0.012675418741511997, 'mean_word_length': 3.072004608294931, 'pattern_counts': {'":': 0, '<': 157, '<?xml version=': 0, '>': 147, 'https://': 8, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5338936', 'n_tokens_mistral': 12800, 'n_tokens_neox': 12366, 'n_words': 2494}