Forum Discussion

hollywild1's avatar
hollywild1
Community Member
1 day ago

Leadership timeline

<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>Leadership Journey Timeline</title>

<link rel="preconnect" href="https://fonts.googleapis.com">

<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,[email protected],500;9..144,600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">

<style>

  :root{

    --ink:#20293a;

    --ink-soft:#5b6577;

    --paper:#f7f5f0;

    --line:#c9c2b3;

    --panel:#ffffff;

    --red:#c0392b;

    --yellow:#c99a1e;

    --green:#1e7a4c;

  }

  *{box-sizing:border-box;}

  body{

    margin:0;

    background:var(--paper);

    font-family:'Inter',system-ui,sans-serif;

    color:var(--ink);

    padding:32px 16px 80px;

  }

  .wrap{max-width:980px;margin:0 auto;}

  header{margin-bottom:6px;}

  h1{

    font-family:'Fraunces',serif;

    font-weight:600;

    font-size:2.1rem;

    margin:0 0 4px;

    outline:none;

    border-bottom:2px solid transparent;

    transition:border-color .15s;

  }

  h1:hover, h1:focus{border-color:var(--line);}

  .subtitle{

    font-size:.92rem;

    color:var(--ink-soft);

    margin:0 0 22px;

    outline:none;

    max-width:640px;

    line-height:1.4;

  }

  .toolbar{

    display:flex;

    gap:10px;

    margin-bottom:18px;

    flex-wrap:wrap;

  }

  button{

    font-family:'Inter',sans-serif;

    font-size:.86rem;

    font-weight:600;

    border:1px solid var(--ink);

    background:var(--ink);

    color:#fff;

    padding:9px 16px;

    border-radius:3px;

    cursor:pointer;

    letter-spacing:.01em;

  }

  button.secondary{background:transparent;color:var(--ink);}

  button.danger{background:transparent;border-color:var(--red);color:var(--red);}

  button:hover{opacity:.85;}

  .chart-card{

    background:var(--panel);

    border:1px solid var(--line);

    border-radius:4px;

    padding:20px 20px 8px;

    position:relative;

  }

  svg{display:block;width:100%;height:auto;overflow:visible;}

  .bead{cursor:grab;touch-action:none;}

  .bead:active{cursor:grabbing;}

  .bead-label{

    font-family:'Inter',sans-serif;

    font-size:12.5px;

    font-weight:600;

    fill:var(--ink);

    pointer-events:none;

  }

  .bead-date{

    font-family:'Inter',sans-serif;

    font-size:10.5px;

    fill:var(--ink-soft);

    pointer-events:none;

  }

  .axis-line{stroke:var(--ink);stroke-width:1.5;}

  .grid-line{stroke:var(--line);stroke-width:1;}

  .axis-caption{

    font-family:'Inter',sans-serif;

    font-size:11px;

    font-weight:600;

    fill:var(--ink-soft);

    letter-spacing:.04em;

  }

  .x-year{

    font-family:'Inter',sans-serif;

    font-size:13px;

    font-weight:600;

    fill:var(--ink);

    cursor:pointer;

  }

  .x-year:hover{fill:var(--ink-soft);}

  .connector{

    fill:none;

    stroke:var(--ink);

    stroke-width:1.6;

    stroke-linejoin:round;

    opacity:.55;

  }

  .year-editor{

    position:absolute;

    display:none;

    width:96px;

    font-family:'Inter',sans-serif;

    font-size:13px;

    font-weight:600;

    text-align:center;

    color:var(--ink);

    border:1px solid var(--ink);

    border-radius:3px;

    padding:4px 6px;

    background:#fff;

    z-index:10;

  }

  .hint{

    font-size:.78rem;

    color:var(--ink-soft);

    margin-top:14px;

    line-height:1.5;

  }

  .legend{

    display:flex;

    gap:18px;

    margin-top:10px;

    font-size:.78rem;

    color:var(--ink-soft);

    align-items:center;

    flex-wrap:wrap;

  }

  .legend span{display:inline-flex;align-items:center;gap:6px;}

  .swatch{width:11px;height:11px;border-radius:50%;display:inline-block;}

 

  /* modal */

  .overlay{

    position:fixed;inset:0;background:rgba(32,41,58,.4);

    display:none;align-items:center;justify-content:center;

    z-index:50;padding:20px;

  }

  .overlay.open{display:flex;}

  .modal{

    background:#fff;

    width:100%;max-width:420px;

    border-radius:6px;

    padding:22px 22px 18px;

    box-shadow:0 12px 40px rgba(0,0,0,.18);

    max-height:90vh;

    overflow-y:auto;

  }

  .modal h2{font-family:'Fraunces',serif;font-size:1.15rem;margin:0 0 14px;}

  .field{margin-bottom:14px;}

  .field label{

    display:block;

    font-size:.76rem;

    font-weight:600;

    color:var(--ink-soft);

    margin-bottom:5px;

    text-transform:uppercase;

    letter-spacing:.04em;

  }

  .field input[type=text], .field textarea{

    width:100%;

    font-family:'Inter',sans-serif;

    font-size:.9rem;

    padding:8px 10px;

    border:1px solid var(--line);

    border-radius:3px;

    resize:vertical;

    color:var(--ink);

  }

  .field textarea{min-height:64px;}

  .charcount{font-size:.72rem;color:var(--ink-soft);text-align:right;margin-top:2px;}

  .icon-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}

  .icon-preview{

    width:36px;height:36px;border-radius:4px;object-fit:contain;

    border:1px solid var(--line);background:#fafafa;

    display:none;

  }

  .icon-preview.show{display:block;}

  .modal-actions{display:flex;justify-content:space-between;margin-top:18px;gap:8px;}

</style>

</head>

<body>

<div class="wrap">

  <header>

    <h1 id="chartTitle" contenteditable="true" spellcheck="false">Leadership Journey Timeline</h1>

    <p class="subtitle" contenteditable="true" spellcheck="false">Plot the highs and lows of your leadership path. Drag each point to where it belongs &mdash; energy on the vertical axis, time across the horizontal.</p>

  </header>

 

  <div class="toolbar">

    <button id="addBeadBtn">+ Add event</button>

    <button id="exportBtn" class="secondary">Export as PNG</button>

  </div>

 

  <div class="chart-card">

    <svg id="chart" viewBox="0 0 1000 560" preserveAspectRatio="xMidYMid meet"></svg>

    <input type="text" class="year-editor" id="yearEditor" maxlength="12">

  </div>

 

  <div class="legend">

    <span><span class="swatch" style="background:var(--green)"></span>High energy (top 40%)</span>

    <span><span class="swatch" style="background:var(--yellow)"></span>Mid (&plusmn;10% of midpoint)</span>

    <span><span class="swatch" style="background:var(--red)"></span>Low energy (bottom 40%)</span>

  </div>

 

  <p class="hint">Drag any point up, down, left, or right &mdash; color updates as it crosses energy zones. Click a point (without dragging) to name the event, add a reflection, or attach a small icon. Click a year along the bottom axis to change it. Nothing saves automatically &mdash; use "Export as PNG" when you're done.</p>

</div>

 

<div class="overlay" id="overlay">

  <div class="modal">

    <h2>Edit event</h2>

    <div class="field">

      <label for="labelInput">Event label (shown on chart)</label>

      <input type="text" id="labelInput" maxlength="40" placeholder="e.g. Promoted to team lead">

      <div class="charcount"><span id="labelCount">0</span>/40</div>

    </div>

    <div class="field">

      <label for="dateInput">Date or note (optional)</label>

      <input type="text" id="dateInput" maxlength="24" placeholder="e.g. 4/3/2018">

    </div>

    <div class="field">

      <label for="detailInput">Longer reflection (optional, kept with this event)</label>

      <textarea id="detailInput" maxlength="400" placeholder="What happened, and why did it move the needle?"></textarea>

    </div>

    <div class="field">

      <label>Icon or small image (optional)</label>

      <div class="icon-row">

        <input type="file" id="iconInput" accept="image/*">

        <img id="iconPreview" class="icon-preview" alt="Icon preview">

        <button type="button" class="secondary" id="removeIconBtn" style="display:none;">Remove</button>

      </div>

    </div>

    <div class="modal-actions">

      <button type="button" class="danger" id="deleteBeadBtn">Delete</button>

      <button type="button" id="closeModalBtn">Done</button>

    </div>

  </div>

</div>

 

<script>

(function(){

  const NS = "http://www.w3.org/2000/svg";

  const svg = document.getElementById('chart');

  const chartCard = document.querySelector('.chart-card');

  const VB_W = 1000, VB_H = 560;

  const plot = { left: 70, right: 970, top: 40, bottom: 470 };

  const LINE_H = 15;

 

  let xYears = ['1991', '2000', '2010', '2018', '2026'];

 

  let beads = [

    { id: newId(), x: 0.12, y: 0.75, label: "Started first team", date: "", detail: "", icon: null },

    { id: newId(), x: 0.38, y: 0.30, label: "Promoted to manager", date: "", detail: "", icon: null },

    { id: newId(), x: 0.68, y: 0.60, label: "Reorg hit the team mid-quarter", date: "", detail: "", icon: null }

  ];

 

  let activeBeadId = null;

 

  function newId(){ return 'b' + Math.random().toString(36).slice(2, 10); }

 

  // ---- Energy zones: red < 40%, yellow 40-60% (±10% of midpoint), green > 60% ----

  function energyColor(yFrac){

    const energyPct = (1 - yFrac) * 100;

    if (energyPct < 40) return { zone:'low', hex:'#c0392b' };

    if (energyPct <= 60) return { zone:'mid', hex:'#c99a1e' };

    return { zone:'high', hex:'#1e7a4c' };

  }

 

  function plotX(xFrac){ return plot.left + xFrac * (plot.right - plot.left); }

  function plotY(yFrac){ return plot.top + yFrac * (plot.bottom - plot.top); }

 

  function svgEl(tag, attrs){

    const el = document.createElementNS(NS, tag);

    for (const k in attrs) el.setAttribute(k, attrs[k]);

    return el;

  }

 

  function starPath(cx, cy, outerR, innerR, points){

    let d = '';

    const step = Math.PI / points;

    for (let i = 0; i < points * 2; i++){

      const r = (i % 2 === 0) ? outerR : innerR;

      const ang = i * step - Math.PI/2;

      d += (i === 0 ? 'M' : 'L') + (cx + r*Math.cos(ang)).toFixed(2) + ',' + (cy + r*Math.sin(ang)).toFixed(2) + ' ';

    }

    return d + 'Z';

  }

 

  // ---- Word-wrap a label into lines (max 3 lines at 40 chars) ----

  function wrapLabel(text, maxChars){

    const words = text.split(/\s+/).filter(Boolean);

    const lines = [];

    let cur = '';

    for (const w of words){

      const trial = cur ? cur + ' ' + w : w;

      if (trial.length <= maxChars){

        cur = trial;

      } else {

        if (cur) lines.push(cur);

        cur = w.length > maxChars ? w.slice(0, maxChars) : w;

      }

    }

    if (cur) lines.push(cur);

    return lines.slice(0, 3);

  }

 

  // ---- Axes, gridlines, editable year labels ----

  function renderAxes(){

    const old = svg.querySelector('#axesGroup');

    if (old) old.remove();

    const g = svgEl('g', { id:'axesGroup' });

 

    for (let i = 0; i <= 4; i++){

      const y = plotY(i / 4);

      g.appendChild(svgEl('line', { class:'grid-line', x1:plot.left, x2:plot.right, y1:y, y2:y }));

    }

 

    g.appendChild(svgEl('line', { class:'axis-line', x1:plot.left, x2:plot.left, y1:plot.top-14, y2:plot.bottom+2 }));

    g.appendChild(svgEl('path', {

      class:'axis-line', fill:'none',

      d:`M ${plot.left-5} ${plot.top-4} L ${plot.left} ${plot.top-16} L ${plot.left+5} ${plot.top-4}`

    }));

    g.appendChild(svgEl('line', { class:'axis-line', x1:plot.left, x2:plot.right+6, y1:plot.bottom, y2:plot.bottom }));

 

    for (let i = 0; i <= 4; i++){

      const x = plotX(i / 4);

      g.appendChild(svgEl('line', { class:'grid-line', x1:x, x2:x, y1:plot.bottom, y2:plot.bottom+6 }));

      const yearText = svgEl('text', {

        class:'x-year', x:x, y:plot.bottom+26, 'text-anchor':'middle', 'data-idx':i

      });

      yearText.textContent = xYears[i];

      yearText.addEventListener('click', function(){ openYearEditor(i, yearText); });

      g.appendChild(yearText);

    }

 

    const high = svgEl('text', { class:'axis-caption', x:plot.left-8, y:plot.top-18, 'text-anchor':'end' });

    high.textContent = 'HIGH';

    g.appendChild(high);

    const low = svgEl('text', { class:'axis-caption', x:plot.left-8, y:plot.bottom+4, 'text-anchor':'end' });

    low.textContent = 'LOW';

    g.appendChild(low);

 

    const vertLabel = svgEl('text', {

      class:'axis-caption', x:22, y:(plot.top+plot.bottom)/2,

      'text-anchor':'middle', transform:`rotate(-90 22 ${(plot.top+plot.bottom)/2})`

    });

    vertLabel.textContent = 'EMOTIONAL ENERGY';

    g.appendChild(vertLabel);

 

    svg.appendChild(g);

  }

 

  function renderConnector(){

    const old = svg.querySelector('#connectorPath');

    if (old) old.remove();

    const sorted = [...beads].sort((a,b) => a.x - b.x);

    if (sorted.length < 2) return;

    let d = '';

    sorted.forEach((b, i) => {

      d += (i === 0 ? 'M' : 'L') + plotX(b.x).toFixed(2) + ',' + plotY(b.y).toFixed(2) + ' ';

    });

    const path = svgEl('path', { id:'connectorPath', class:'connector', d:d.trim() });

    const axesGroup = svg.querySelector('#axesGroup');

    svg.insertBefore(path, axesGroup ? axesGroup.nextSibling : svg.firstChild);

  }

 

  function renderBeads(){

    svg.querySelectorAll('.bead-group').forEach(el => el.remove());

    const sorted = [...beads].sort((a,b) => a.x - b.x);

 

    sorted.forEach((bead, idx) => {

      const cx = plotX(bead.x), cy = plotY(bead.y);

      const info = energyColor(bead.y);

      const g = svgEl('g', { class:'bead-group', 'data-id':bead.id });

 

      let marker;

      if (info.zone === 'low'){

        marker = svgEl('path', { class:'bead', d: starPath(cx, cy, 12, 5.5, 7), fill: info.hex });

      } else if (info.zone === 'high'){

        marker = svgEl('path', { class:'bead', d: starPath(cx, cy, 11, 8, 9), fill: info.hex });

      } else {

        marker = svgEl('circle', { class:'bead', cx, cy, r:9, fill: info.hex });

      }

      g.appendChild(marker);

 

      const labelAbove = idx % 2 === 0;

      const labelX = Math.min(Math.max(cx, 85), 915);

      const lines = bead.label ? wrapLabel(bead.label, 18) : [];

      const n = lines.length;

 

      let firstLineY;

      if (labelAbove){

        firstLineY = cy - 22 - (n - 1) * LINE_H;

      } else {

        firstLineY = cy + 30;

      }

 

      if (n > 0){

        const text = svgEl('text', { class:'bead-label', x:labelX, y:firstLineY, 'text-anchor':'middle' });

        lines.forEach((line, li) => {

          const tspan = svgEl('tspan', { x:labelX, dy: li === 0 ? 0 : LINE_H });

          tspan.textContent = line;

          text.appendChild(tspan);

        });

        g.appendChild(text);

 

        if (bead.date){

          const dateY = labelAbove ? firstLineY - LINE_H : firstLineY + n * LINE_H;

          const dateText = svgEl('text', { class:'bead-date', x:labelX, y:dateY, 'text-anchor':'middle' });

          dateText.textContent = bead.date;

          g.appendChild(dateText);

        }

      }

 

      if (bead.icon){

        const size = 26;

        const iconY = labelAbove ? cy + 32 : cy - 26;

        g.appendChild(svgEl('image', {

          x: cx - size/2, y: iconY - size/2, width:size, height:size, href: bead.icon

        }));

      }

 

      attachDrag(g, bead);

      svg.appendChild(g);

    });

  }

 

  function renderAll(){

    renderAxes();

    renderConnector();

    renderBeads();

  }

 

  function clientToSvgFrac(clientX, clientY){

    const pt = svg.createSVGPoint();

    pt.x = clientX; pt.y = clientY;

    const loc = pt.matrixTransform(svg.getScreenCTM().inverse());

    return {

      xFrac: Math.max(0, Math.min(1, (loc.x - plot.left) / (plot.right - plot.left))),

      yFrac: Math.max(0, Math.min(1, (loc.y - plot.top) / (plot.bottom - plot.top)))

    };

  }

 

  // ---- Drag with movement threshold + rAF throttle ----

  function attachDrag(g, bead){

    g.addEventListener('pointerdown', function(e){

      e.preventDefault();

      const startX = e.clientX, startY = e.clientY;

      let moved = false;

      let lastEvt = null;

      let raf = null;

 

      function applyMove(evt){

        const { xFrac, yFrac } = clientToSvgFrac(evt.clientX, evt.clientY);

        bead.x = xFrac; bead.y = yFrac;

        renderConnector();

        renderBeads();

      }

      function onMove(evt){

        if (!moved){

          if (Math.hypot(evt.clientX - startX, evt.clientY - startY) < 4) return;

          moved = true;

        }

        lastEvt = evt;

        if (!raf){

          raf = requestAnimationFrame(function(){

            raf = null;

            if (lastEvt) applyMove(lastEvt);

          });

        }

      }

      function onUp(){

        document.removeEventListener('pointermove', onMove);

        document.removeEventListener('pointerup', onUp);

        document.removeEventListener('pointercancel', onUp);

        if (!moved) openModal(bead.id);

      }

      document.addEventListener('pointermove', onMove);

      document.addEventListener('pointerup', onUp);

      document.addEventListener('pointercancel', onUp);

    });

  }

 

  // ---- Year label editor ----

  const yearEditor = document.getElementById('yearEditor');

  let editingYearIdx = null;

 

  function openYearEditor(idx, textEl){

    editingYearIdx = idx;

    const cardRect = chartCard.getBoundingClientRect();

    const tRect = textEl.getBoundingClientRect();

    yearEditor.style.left = (tRect.left + tRect.width/2 - cardRect.left - 48) + 'px';

    yearEditor.style.top = (tRect.top - cardRect.top - 6) + 'px';

    yearEditor.value = xYears[idx];

    yearEditor.style.display = 'block';

    yearEditor.focus();

    yearEditor.select();

  }

  function commitYear(){

    if (editingYearIdx === null) return;

    xYears[editingYearIdx] = yearEditor.value.trim();

    editingYearIdx = null;

    yearEditor.style.display = 'none';

    renderAxes();

  }

  yearEditor.addEventListener('blur', commitYear);

  yearEditor.addEventListener('keydown', function(e){

    if (e.key === 'Enter') yearEditor.blur();

    if (e.key === 'Escape'){ editingYearIdx = null; yearEditor.style.display = 'none'; }

  });

 

  // ---- Modal ----

  const overlay = document.getElementById('overlay');

  const labelInput = document.getElementById('labelInput');

  const labelCount = document.getElementById('labelCount');

  const dateInput = document.getElementById('dateInput');

  const detailInput = document.getElementById('detailInput');

  const iconInput = document.getElementById('iconInput');

  const iconPreview = document.getElementById('iconPreview');

  const removeIconBtn = document.getElementById('removeIconBtn');

 

  function findBead(id){ return beads.find(b => b.id === id); }

 

  function openModal(id){

    activeBeadId = id;

    const bead = findBead(id);

    if (!bead) return;

    labelInput.value = bead.label || '';

    labelCount.textContent = labelInput.value.length;

    dateInput.value = bead.date || '';

    detailInput.value = bead.detail || '';

    if (bead.icon){

      iconPreview.src = bead.icon;

      iconPreview.classList.add('show');

      removeIconBtn.style.display = 'inline-block';

    } else {

      iconPreview.classList.remove('show');

      removeIconBtn.style.display = 'none';

    }

    overlay.classList.add('open');

    setTimeout(() => labelInput.focus(), 50);

  }

  function closeModal(){

    overlay.classList.remove('open');

    activeBeadId = null;

  }

 

  labelInput.addEventListener('input', function(){

    labelCount.textContent = labelInput.value.length;

    const bead = findBead(activeBeadId);

    if (bead){ bead.label = labelInput.value; renderBeads(); }

  });

  dateInput.addEventListener('input', function(){

    const bead = findBead(activeBeadId);

    if (bead){ bead.date = dateInput.value; renderBeads(); }

  });

  detailInput.addEventListener('input', function(){

    const bead = findBead(activeBeadId);

    if (bead){ bead.detail = detailInput.value; }

  });

 

  // Downscale uploaded icons so data URLs stay small

  function downscaleImage(dataUrl, maxPx, cb){

    const img = new Image();

    img.onload = function(){

      const scale = Math.min(1, maxPx / Math.max(img.width, img.height));

      const w = Math.max(1, Math.round(img.width * scale));

      const h = Math.max(1, Math.round(img.height * scale));

      const c = document.createElement('canvas');

      c.width = w; c.height = h;

      c.getContext('2d').drawImage(img, 0, 0, w, h);

      cb(c.toDataURL('image/png'));

    };

    img.onerror = function(){ cb(dataUrl); };

    img.src = dataUrl;

  }

 

  iconInput.addEventListener('change', function(){

    const file = iconInput.files[0];

    if (!file) return;

    const beadId = activeBeadId;

    const reader = new FileReader();

    reader.onload = function(e){

      downscaleImage(e.target.result, 128, function(smallUrl){

        const bead = findBead(beadId);

        if (bead){

          bead.icon = smallUrl;

          if (activeBeadId === beadId){

            iconPreview.src = smallUrl;

            iconPreview.classList.add('show');

            removeIconBtn.style.display = 'inline-block';

          }

          renderBeads();

        }

      });

    };

    reader.readAsDataURL(file);

    iconInput.value = '';

  });

  removeIconBtn.addEventListener('click', function(){

    const bead = findBead(activeBeadId);

    if (bead){

      bead.icon = null;

      iconPreview.classList.remove('show');

      removeIconBtn.style.display = 'none';

      renderBeads();

    }

  });

  document.getElementById('deleteBeadBtn').addEventListener('click', function(){

    beads = beads.filter(b => b.id !== activeBeadId);

    closeModal();

    renderAll();

  });

  document.getElementById('closeModalBtn').addEventListener('click', closeModal);

  overlay.addEventListener('click', function(e){

    if (e.target === overlay) closeModal();

  });

  document.addEventListener('keydown', function(e){

    if (e.key === 'Escape' && overlay.classList.contains('open')) closeModal();

  });

 

  // New beads land at the end of the string, past the current rightmost point.

  // When space runs out, they tuck into the remaining gap rather than piling up.

  function nextBeadPosition(){

    if (beads.length === 0) return { x: 0.06, y: 0.5 };

    let last = beads[0];

    for (const b of beads){ if (b.x > last.x) last = b; }

    const room = 1 - last.x;

    const x = room < 0.005 ? 1 : Math.min(last.x + 0.14, last.x + room * 0.6);

    return { x, y: last.y };

  }

 

  document.getElementById('addBeadBtn').addEventListener('click', function(){

    const pos = nextBeadPosition();

    const newBead = { id: newId(), x: pos.x, y: pos.y, label: 'New event', date: '', detail: '', icon: null };

    beads.push(newBead);

    renderAll();

    openModal(newBead.id);

  });

 

  // ---- PNG export: embedded styles + title band ----

  const EXPORT_FONTS = "Arial, Helvetica, sans-serif";

  const EXPORT_CSS = `

    .axis-line{stroke:#20293a;stroke-width:1.5;fill:none;}

    .grid-line{stroke:#c9c2b3;stroke-width:1;}

    .connector{fill:none;stroke:#20293a;stroke-width:1.6;stroke-linejoin:round;opacity:.55;}

    .bead-label{font-family:${EXPORT_FONTS};font-size:12.5px;font-weight:600;fill:#20293a;}

    .bead-date{font-family:${EXPORT_FONTS};font-size:10.5px;fill:#5b6577;}

    .axis-caption{font-family:${EXPORT_FONTS};font-size:11px;font-weight:600;fill:#5b6577;letter-spacing:.04em;}

    .x-year{font-family:${EXPORT_FONTS};font-size:13px;font-weight:600;fill:#20293a;}

  `;

 

  document.getElementById('exportBtn').addEventListener('click', function(){

    const clone = svg.cloneNode(true);

    clone.setAttribute('xmlns', NS);

    clone.setAttribute('width', VB_W);

    clone.setAttribute('height', VB_H);

 

    const styleEl = document.createElementNS(NS, 'style');

    styleEl.textContent = EXPORT_CSS;

    clone.insertBefore(styleEl, clone.firstChild);

 

    const bg = svgEl('rect', { x:0, y:0, width:VB_W, height:VB_H, fill:'#ffffff' });

    clone.insertBefore(bg, styleEl.nextSibling);

 

    const svgStr = new XMLSerializer().serializeToString(clone);

    const url = URL.createObjectURL(new Blob([svgStr], { type:'image/svg+xml;charset=utf-8' }));

 

    const img = new Image();

    img.onload = function(){

      const scale = 2;

      const TITLE_BAND = 72;

      const canvas = document.createElement('canvas');

      canvas.width = VB_W * scale;

      canvas.height = (VB_H + TITLE_BAND) * scale;

      const ctx = canvas.getContext('2d');

      ctx.scale(scale, scale);

      ctx.fillStyle = '#ffffff';

      ctx.fillRect(0, 0, VB_W, VB_H + TITLE_BAND);

 

      const title = (document.getElementById('chartTitle').textContent || '').trim();

      if (title){

        ctx.fillStyle = '#20293a';

        ctx.font = '600 30px Georgia, "Times New Roman", serif';

        ctx.textAlign = 'center';

        ctx.fillText(title, VB_W / 2, 48);

      }

 

      ctx.drawImage(img, 0, TITLE_BAND, VB_W, VB_H);

      URL.revokeObjectURL(url);

      canvas.toBlob(function(blob){

        const link = document.createElement('a');

        link.download = 'leadership-journey-timeline.png';

        link.href = URL.createObjectURL(blob);

        link.click();

      }, 'image/png');

    };

    img.onerror = function(){

      URL.revokeObjectURL(url);

      alert('Export failed to render. If you added an icon image, try removing it and exporting again.');

    };

    img.src = url;

  });

 

  renderAll();

})();

</script>

</body>

</html>

 

1 Reply