//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//
//     Sample Code
//
//!    Work with data spanning multiple levels.
import KeyLines from "keylines";
import data from "./sequential-data.js";

let chart;
let generatedData;
let maxLevels;
const options = {
  multiComp: false,
};

const linkShapeInputs = [
  ...document.querySelectorAll('input[name="linkshape"]'),
];
const orientationInputs = [
  ...document.querySelectorAll('input[name="orientation"]'),
];
const generateDataEl = document.getElementById("generate");
const levelOrderingInputs = [
  ...document.querySelectorAll('input[name="levelOrdering"]'),
];
const orderByInputs = [...document.querySelectorAll('input[name="orderBy"]')];
const stretchSliderEl = document.getElementById("stretchSlider");
const stretchSliderInput = document.querySelector('input[name="stretch"]');

// Return a colour gradient scale by adjusting the alpha value based on the max levels
function getColourScale() {
  const colourScale = [];
  const startColour = { r: 0, g: 201, b: 128 };
  const endColour = { r: 255, g: 255, b: 255 };

  const deltaR = (endColour.r - startColour.r) / maxLevels;
  const deltaG = (endColour.g - startColour.g) / maxLevels;
  const deltaB = (endColour.b - startColour.b) / maxLevels;

  colourScale.push(`rgb(${startColour.r}, ${startColour.g}, ${startColour.b})`);
  for (let i = 1; i <= maxLevels; i++) {
    colourScale.push(
      `rgb(${Math.round(startColour.r + deltaR * i)}, ${Math.round(
        startColour.g + deltaG * i
      )}, ${Math.round(startColour.b + deltaB * i)})`
    );
  }

  return colourScale;
}

function resetLevelColourScale(isUniform) {
  const colourScale = getColourScale();
  const props = [];
  chart.each({ type: "node" }, ({ id, d }) =>
    props.push(
      isUniform
        ? // Revert to the same colour for all nodes
          { id, c: "rgb(0, 201, 128)", fc: "rgba(0,0,0,0)" }
        : // Set the colour scale on the level assignment
          { id, c: colourScale[d.level], fc: "rgb(0,0,0)" }
    )
  );
  chart.setProperties(props);
}

function resetGlyphs(show) {
  // rescale number glyphs to distinguish them in the UI from level numbers
  const scale = 10;
  const props = [];

  chart.each({ type: "node" }, ({ id, d }) => {
    const g =
      show && d.col !== void 0
        ? [
            {
              id,
              t: d.col * scale,
              c: "rgb(128, 0, 0)",
              p: 45,
              r: 40,
              e: 3,
              b: null,
            },
          ]
        : null;
    props.push({ id, g });
  });

  chart.setProperties(props);
}

// Runs a sequential layout from the selected layout options
function runLayout() {
  // Collect the layout options selected
  const selectedNodes = chart
    .selection()
    .filter((item) => chart.getItem(item).type === "node");
  const orientation = document.querySelector(
    'input[name="orientation"]:checked'
  ).value;
  const stretch = document.querySelector('input[name="stretch"]').value;
  const linkShape = document.querySelector('input[name="linkshape"]:checked')
    .value;
  const layoutOpts = { stretch, orientation, linkShape };

  // Get the current level ordering criteria
  const orderMethod = document.querySelector(
    'input[name="levelOrdering"]:checked'
  ).value;

  if (orderMethod === "levels") {
    // A colour gradient is used to highlight the ordering
    resetLevelColourScale(false);
    // The level value is used to assign levels
    layoutOpts.level = "level";
  } else {
    resetLevelColourScale(true);
    if (orderMethod === "top" && selectedNodes.length > 0) {
      // Selected nodes from the chart will be placed at the top of the hierarchy
      layoutOpts.top = selectedNodes;
    }
    // The default is auto levels are used to assign levels
  }

  // always show orderBy glyphs
  resetGlyphs(true);

  // Get the current column ordering criteria
  const orderBy = document.querySelector('input[name="orderBy"]:checked');
  layoutOpts.orderBy = {
    property: orderBy.getAttribute("property"),
    sortBy: orderBy.getAttribute("sortBy"),
  };

  chart.layout("sequential", layoutOpts);
}

function generateChartData() {
  // Decide how many components we want
  // Bias towards just a single component and occasionally leave it to fate
  // Always display a single component on load
  if (options.multiComp && Math.random() > 0.6) {
    options.comps = Math.ceil(Math.random() * 3);
  } else {
    options.comps = 1;
  }

  generatedData = data.generate(options);
  // Get the max number of levels generated
  maxLevels = generatedData.chartLevel;
  chart.load(generatedData.chartData);
  options.multiComp = true;
  runLayout();
}

function marqueeDragStartHandler({ type }) {
  if (type === "marquee") {
    chart.off("selection-change", runLayout);
  }
}

function marqueeDragEndHandler({ type }) {
  if (type === "marquee") {
    chart.on("selection-change", runLayout);
    runLayout();
  }
}

function levelOrderingHandler(e) {
  if (e.target.value === "top") {
    chart.on("drag-start", marqueeDragStartHandler);
    chart.on("drag-end", marqueeDragEndHandler);
    chart.on("selection-change", runLayout);
  } else {
    chart.off("drag-start", marqueeDragStartHandler);
    chart.off("drag-end", marqueeDragEndHandler);
    chart.off("selection-change", runLayout);
  }
  runLayout();
}

function addEventHandlers() {
  chart.on("drag-start", ({ type, setDragOptions }) => {
    if (type === "node") {
      setDragOptions({ type: "pan" });
    }
  });

  // Generate a new chart
  generateDataEl.addEventListener("click", generateChartData);

  // Change link style
  linkShapeInputs.forEach((input) => {
    input.addEventListener("change", () => {
      runLayout();
    });
  });

  stretchSliderInput.addEventListener("change", () => {
    // Update the slider value on the UI
    const sliderValue = stretchSliderEl.value;
    document.getElementById("stretchVal").innerHTML = ` ${sliderValue}`;
    runLayout();
  });

  orientationInputs.forEach((input) => {
    input.addEventListener("change", () => {
      runLayout();
    });
  });

  levelOrderingInputs.forEach((input) => {
    input.addEventListener("change", levelOrderingHandler);
  });

  orderByInputs.forEach((input) => {
    input.addEventListener("change", runLayout);
  });
}

async function startKeyLines() {
  const chartOptions = {
    logo: { u: "/images/Logo.png" },
    handMode: true,
    selectedNode: {
      ha0: {
        c: "rgb(255, 156, 161)",
        w: 10,
        r: 40,
      },
    },
  };

  chart = await KeyLines.create({
    container: "klchart",
    options: chartOptions,
  });

  addEventHandlers();
  generateChartData();
}

window.addEventListener("DOMContentLoaded", startKeyLines);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
  <head>
    <meta charset="UTF-8">
    <title>Display Hierarchies</title>
    <link rel="stylesheet" type="text/css" href="/css/keylines.css">
    <link rel="stylesheet" type="text/css" href="/css/minimalsdk.css">
    <link rel="stylesheet" type="text/css" href="/css/sdk-layout.css">
    <link rel="stylesheet" type="text/css" href="/css/demo.css">
    <script src="/vendor/jquery.js" defer type="text/javascript"></script>
    <script id="keylines" src="/public/keylines.umd.cjs" defer type="text/javascript"></script>
    <script src="/sequential.js" crossorigin="use-credentials" type="module"></script>
  </head>
  <body>
    <div class="chart-wrapper demo-cols">
      <div class="tab-content-panel flex1" id="lhs" data-tab-group="rhs">
        <div class="toggle-content is-visible" id="lhsVisual" style="width:100%; height: 100%;">
          <div class="klchart" id="klchart">
          </div>
        </div>
      </div>
      <div class="rhs citext closed" id="demorhscontent">
        <div class="title-bar">
          <svg viewBox="0 0 360 60" style="max-width: 360px; max-height: 60px; font-size: 24px; font-family: Raleway; flex: 1;">
            <text x="0" y="38" style="width: 100%;">Display Hierarchies</text>
          </svg>
        </div>
        <div class="tab-content-panel" data-tab-group="rhs">
          <div class="toggle-content is-visible tabcontent" id="controlsTab">
            <p>Use the sequential layout to display ordered hierarchies and customise it with various options.</p>
            <form autocomplete="off" onsubmit="return false" id="rhsForm">
              <div class="cicontent">
                <input class="btn btn-kl" type="button" value="Generate" style="margin: 3px 0px 5px;" id="generate">
                <fieldset>
                  <legend>Layout Options</legend>
                  <h5 style="margin-top:1px">Order of levels</h5>
                  <radio class="inline">
                    <input type="radio" name="levelOrdering" value="auto" checked="checked"><span> Auto</span>
                  </radio>
                  <radio class="inline">
                    <input type="radio" name="levelOrdering" value="top"><span> Selected nodes first</span>
                  </radio>
                  <radio class="inline">
                    <input type="radio" name="levelOrdering" value="levels"><span> Predefined</span>
                  </radio>
                  <h5 style="margin-top:10px">Order within levels</h5>
                  <radio class="inline">
                    <input type="radio" name="orderBy" value="auto" checked="checked"><span> Auto</span>
                  </radio>
                  <radio class="inline">
                    <input type="radio" name="orderBy" property="col" sortBy="descending"><span> Descending</span>
                  </radio>
                  <radio class="inline">
                    <input type="radio" name="orderBy" property="col" sortBy="ascending"><span> Ascending</span>
                  </radio>
                  <h5>Stretch distance between levels: <span style="font-weight: normal" id="stretchVal">1.5</span></h5>
                  <input id="stretchSlider" type="range" name="stretch" min="1" max="4" value="1.5" step="0.1">
                  <h5>Orientation</h5>
                  <radio class="inline">
                    <input type="radio" name="orientation" value="down" checked="checked"><span>Down</span>
                  </radio>
                  <radio class="inline">
                    <input type="radio" name="orientation" value="up"><span>Up</span>
                  </radio>
                  <radio class="inline">
                    <input type="radio" name="orientation" value="left"><span>Left</span>
                  </radio>
                  <radio class="inline">
                    <input type="radio" name="orientation" value="right"><span>Right</span>
                  </radio>
                  <h5>Link Shape</h5>
                  <radio class="inline">
                    <input type="radio" name="linkshape" value="curved" checked="checked"><span>Curved</span>
                  </radio>
                  <radio class="inline">
                    <input type="radio" name="linkshape" value="direct"><span>Direct</span>
                  </radio>
                </fieldset>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
    <div id="moreParent">
      <div id="moreContainer">
      </div>
    </div>
    <div id="lazyScripts">
    </div>
  </body>
</html>
Loading source