qlik-oss/sn-org-chart

View on GitHub
src/tree/transform.js

Summary

Maintainability
A
45 mins
Test Coverage
D
65%
import { select, zoom, zoomIdentity } from "d3";
import constants from "./size-constants";
import { closeTooltip } from "./tooltip";

export const getBBoxOfNodes = (nodes) => {
  const { cardWidth, cardHeight, buttonHeight, buttonMargin } = constants;
  const bbox = {
    left: Infinity,
    top: Infinity,
    right: -Infinity,
    bottom: -Infinity,
  };
  nodes.forEach((node) => {
    bbox.left = Math.min(node.xActual, bbox.left);
    bbox.top = Math.min(node.yActual, bbox.top);
    bbox.right = Math.max(node.xActual, bbox.right);
    bbox.bottom = Math.max(node.yActual, bbox.bottom);
  });
  return {
    x: bbox.left,
    y: bbox.top - buttonHeight - buttonMargin,
    width: bbox.right - bbox.left + cardWidth,
    height: bbox.bottom - bbox.top + cardHeight + (buttonHeight + buttonMargin) * 2,
  };
};

export const getInitialZoomState = (bBox, element, navigationMode) => {
  const { widthMargin, cardHeight, minZoom } = constants;
  const maxZoom = navigationMode === "expandAll" ? Infinity : constants.maxZoom;
  const { width, height } = bBox;
  const { clientHeight, clientWidth } = element;
  const calcWidth = width + 2 * widthMargin;
  const calcHeight = height + cardHeight;
  const xZoom = Math.max(Math.min(calcWidth / clientWidth, maxZoom), minZoom);
  const yZoom = Math.max(Math.min(calcHeight / clientHeight, maxZoom), minZoom);
  if (xZoom > yZoom) {
    // Zooming for x direction
    return {
      initialX: -bBox.x + widthMargin,
      initialY: -bBox.y + (clientHeight * xZoom - height) / 2,
      initialZoom: xZoom,
    };
  }
  // Zooming for y direction
  return {
    initialX: -bBox.x + (clientWidth * yZoom - width) / 2,
    initialY: cardHeight / 2,
    initialZoom: yZoom,
  };
};

export const getTranslations = (bBox, height, width) => {
  const scaleToWidth = bBox.width / width > bBox.height / height;
  const scaleFactor = scaleToWidth ? bBox.width / width : bBox.height / height;
  const translations = { scaleFactor };
  if (scaleToWidth) {
    const yTrans = -bBox.y + (height * scaleFactor - bBox.height) / 2;
    translations.divTranslation = `${-bBox.x}px, ${yTrans}px`;
  } else {
    const xTrans = -bBox.x + (width * scaleFactor - bBox.width) / 2;
    translations.divTranslation = `${xTrans}px, ${-bBox.y}px`;
  }

  return translations;
};

export const applyTransform = (eventTransform, svg, divBox, width, height) => {
  const scaleFactor = eventTransform.k;
  const translation = `${eventTransform.x}px, ${eventTransform.y}px`;

  svg.attr("transform", eventTransform);
  divBox.classed("org-disable-transition", true);
  svg.classed("org-disable-transition", true);

  divBox.attr(
    "style",
    `width:${width}px;height:${height}px; transform: translate(${translation}) scale(${scaleFactor})`,
  );
};

export function setZooming({
  containerData,
  setTransform,
  transformState,
  wrapperState,
  initialZoomState,
  navigationMode,
}) {
  const { svg, divBox, width, height, zoomWrapper, element, tooltip, homeButton } = containerData;
  const { x = 0, y = 0 } = transformState;
  const { minZoom, maxZoom } = constants;
  const zoomFactor = (transformState && 1 / transformState.zoom) || initialZoomState.initialZoom;
  const scaleFactor = navigationMode === "expandAll" ? zoomFactor : Math.max(Math.min(maxZoom, zoomFactor), minZoom);

  // sends otherwise captured mouse event to handle context menu correctly in sense
  const bubbleEvent = () => {
    // eslint-disable-next-line no-undef
    const newEvent = document.createEvent("MouseEvents");
    newEvent.initEvent("mousedown", true, false);
    element.dispatchEvent(newEvent);
  };

  const zoomed = (event) => {
    select(homeButton).attr("class", "sn-org-homebutton lui-fade-button lui-fade-button--large");
    setTransform({
      zoom: event.transform.k / scaleFactor,
      x: event.transform.x,
      y: event.transform.y,
    });
    bubbleEvent();
    closeTooltip(tooltip);
    applyTransform(
      zoomIdentity.translate(event.transform.x, event.transform.y).scale(event.transform.k / scaleFactor),
      svg,
      divBox,
      width,
      height,
    );
  };

  select(zoomWrapper).call(
    zoom()
      .extent([
        [0, 0],
        [width, height],
      ])
      .filter(
        (event) =>
          !wrapperState.constraints.active &&
          event.type !== "dblclick" &&
          !(event.type === "mousedown" && event.which === 3),
      )
      .scaleExtent([navigationMode === "expandAll" ? 0.8 : minZoom * scaleFactor, maxZoom * scaleFactor])
      .on("start", bubbleEvent)
      .on("zoom", zoomed),
  );

  setTransform({ zoom: 1 / scaleFactor, x, y });
  applyTransform(zoomIdentity.translate(x, y).scale(1 / scaleFactor), svg, divBox, width, height);
}

export const getSnapshotZoom = (rect, viewState, initialTransform) => {
  if (!viewState) {
    return zoomIdentity.translate(initialTransform.x, initialTransform.y).scale(initialTransform.zoom);
  }
  const { size } = viewState;
  const snapZoom = rect.width / size.w > rect.height / size.h ? rect.height / size.h : rect.width / size.w;
  const newX = viewState.transform.x * snapZoom;
  const newY = viewState.transform.y * snapZoom;
  const newZoom = viewState.transform.zoom * snapZoom;
  return zoomIdentity.translate(newX, newY).scale(newZoom);
};

export default function transform(nodes, width, height, svg, divBox, useTransitions) {
  // Zooming and positioning of the tree
  const bBox = getBBoxOfNodes(nodes);
  const { divTranslation, scaleFactor } = getTranslations(bBox, height, width);

  svg.attr("style", `transform: scale(${1 / scaleFactor}) translate(${divTranslation});`);
  divBox.classed("org-disable-transition", !useTransitions);
  svg.classed("org-disable-transition", !useTransitions);
  divBox.attr(
    "style",
    `width:${width}px;height:${height}px;
      transform: scale(${1 / scaleFactor}) translate(${divTranslation});`,
  );
}