Стиль частично применяется к узлу дерева при использовании d3 SVG и iOS 12 Ioni c AngularJS - PullRequest
0 голосов
/ 29 января 2020

Привет, добрые люди из StackOverflow.

У меня довольно серьезная ошибка. Я использую SVG в приложении Ioni c и использую d3 для построения дерева решений. Проблема в том, что когда бы я ни пытался применить стиль / класс к узлу дерева, он либо применяется / частично применяется / вообще не применяется. Я перепробовал много решений и методов отладки, но безрезультатно. Эта проблема встречается только в iOS <= 12. Она работает безупречно на android и iOS 13. Я прикрепил изображения правильно отрендеренного и неправильного узла. </p>

Директива:

angular.module("vshare.decisionTree", []).directive("decisionTree", function() {
  return {
    restrict: "E",
    transclude: "true",
    template:
      "<svg xmlns='http://www.w3.org/2000/svg' version='1.1' id='dt-{{::$id}}' ng-transclude class='treeContainer'>A:{{internalControl}}</svg>",
    scope: {
      control: "="
    },
    link: function(scope, elem, attrs) {
      scope.internalControl = scope.control || {};
      scope.internalControl.highlightNode = function(data) {
        highlightNode(data, "#dt-" + scope.$id);
      };
      setTimeout(function() {
        var svg = d3.select("#dt-" + scope.$id);
        init(JSON.parse(attrs.json), svg, true, "dt-" + scope.$id);
      }, 50);
    }
  };
});

JS Код для дерева решений: // Горизонтальная ось x, а вертикальная y

var treeState = [];
var flatTree = [];
var NODE_WIDTH = 150;
var NODE_TEXT_WIDTH = 100;
var LEVEL_MARGIN = NODE_WIDTH + 100;
var NODE_VERT_PADDING = 50;
var NODE_VERT_MARGIN = 50;
var CONTAINER_ID;

function init(data, svg, isPreview, containerId) {
  //Flatten the D tree for Search and store it in the flatTree variable (after resetting it)
  flatTree = [];
  flattenTree(data[0]);
  treeState = data;
  CONTAINER_ID = containerId;
  drawSiblingNodes(treeState, svg);
  onNodeClick(treeState[0], svg, 0);
  svg.on("click", function() {
    clearAttachmentPopups();
  });
  if (isPreview) {
    removeColorAndIndicatorsFromNodes();
  }
}

function flattenTree(currentNode) {
  var i, currentChild, result;
  flatTree.push({ node_id: currentNode.node_id, label: currentNode.label });
  for (i = 0; i < currentNode.children.length; i++) {
    currentChild = currentNode.children[i];
    result = flattenTree(currentChild);
    if (result !== false) {
      return result;
    }
  }
  return false;
}

function drawNode(components) {
  var svg = components.svg;
  var level = components.level;
  var node = components.sibling;
  var isFirstChild = components.isFirstChild;
  var lastNodeBBox = components.bBox;
  var nodeStartY = lastNodeBBox ? lastNodeBBox.y + lastNodeBBox.height : 0;
  var id = "N" + node.node_id;
  var nodeGroup = svg.append("g").attr("id", id);
  // var text = "Level " + level + " " + node.label;
  var text = node.label;
  var nodeHeight = 0;

  nodeGroup.append("rect").attr("class", "rect");

  // make text and wrap it and get the BBox
  var textBBox = nodeGroup
    .append("text")
    .attr("x", 10)
    .attr("y", 20)
    .text(text)
    .call(wrap, NODE_TEXT_WIDTH)
    .node()
    .getBBox();
  //now we know the hight to give of out node
  nodeHeight = textBBox.height + NODE_VERT_PADDING;

  //set height of rect + some padding
  d3.select("#" + CONTAINER_ID + " " + "#" + id + " rect")
    .attr("height", nodeHeight)
    .attr("width", NODE_WIDTH);
  var vertMargin = isFirstChild ? 20 : NODE_VERT_MARGIN;
  var translateX = level * LEVEL_MARGIN;
  var translateY = nodeStartY + vertMargin;
  nodeGroup.attr(
    "transform",
    "translate(" + translateX + "," + translateY + ")"
  );

  //make the node bounding box info
  var nodeBBox = {
    x: level * LEVEL_MARGIN,
    y: translateY,
    height: nodeHeight,
    width: NODE_WIDTH
  };

  // draw connector to parent
  // connector is only required if its a child
  if (node.parent_id) {
    var parent = findNodeById(treeState[0], node.parent_id);
    var parentBBox = parent.bBox;
    drawConnector(svg, parentBBox, nodeBBox, node.node_id);
  }

  //Draw Pointer
  if (node.children && node.children.length > 0) {
    drawPointer(svg, nodeBBox, node.node_id);
  }

  //Add this node to our state object.
  node.isVisible = true;
  node.bBox = nodeBBox;

  //on click function
  nodeGroup.on("click", () => {
    onNodeClick(node, svg, level);
  });

  // Attachment image
  if (
    (node.attachment && node.attachment.length > 0) ||
    (node.link && node.link.length > 0)
  ) {
    var attachmentButton = nodeGroup
      .append("svg:image")
      .attr("x", NODE_WIDTH - 25)
      .attr("y", 5)
      .attr("width", 20)
      .attr("height", 24)
      .attr("xlink:href", "./img/decisionTree/attach.png");
    attachmentButton.on("click", () => {
      onAttachmentClick(node, svg);
      d3.event.stopPropagation();
    });
  }
  resizeSvg(nodeGroup, svg);
  return nodeBBox;
}

function onNodeClick(node, svg, level) {
  if (node.parent_id) {
    eraseVisibleSubtreeOfChildren(treeState, node.parent_id);
  } else {
    eraseVisibleSubtreeOfChildren(treeState, node.node_id);
  }
  drawSiblingNodes(node.children, svg, level + 1);
  clearAllPaths();
  updateAllNodeColorsById(node.node_id, treeState);
  //Draw Metadata indicators
  drawMetadataIndicators(svg, node, node.bBox);
}

function drawSiblingNodes(arrOfSiblings, svg, level = 0) {
  var sibling, bBox, isFirstChild;
  for (var i = 0; i < arrOfSiblings.length; i++) {
    sibling = arrOfSiblings[i];
    isFirstChild = i === 0;
    bBox = drawNode({
      svg: svg,
      level: level,
      sibling: sibling,
      isFirstChild: isFirstChild,
      bBox: bBox
    });
  }
}

//*********************************************//

function onAttachmentClick(node, svg) {
  var nodeBBox = node.bBox;
  var attachBox = svg.append("g").attr("id", "attachment");
  var hasLink = node.link.length > 0;
  var hasAttachment = node.attachment.length > 0;
  var POPUP_HEIGHT = 40;

  var xOffset = nodeBBox.x + 20;
  var yOffset = nodeBBox.y + 20;

  if (hasLink && hasAttachment) {
    POPUP_HEIGHT = 80;
  }

  attachBox
    .append("rect")
    .attr("width", NODE_WIDTH + 50)
    .attr("height", POPUP_HEIGHT)
    .attr("class", "attach")
    .attr("x", xOffset)
    .attr("y", yOffset);

  if (hasLink) {
    attachBox
      .append("svg:image")
      .attr("x", xOffset + 10)
      .attr("y", yOffset + 10)
      .attr("width", 20)
      .attr("height", 24)
      .attr("xlink:href", "./img/decisionTree/link.png");
    attachBox
      .append("text")
      .attr("class", "attachText")
      .attr("x", xOffset + 40)
      .attr("y", yOffset + 30)
      .text("View Link")
      .on("click", function() {
        clearAttachmentPopups();
        alert("View Link");
      });
  }

  if (hasAttachment && hasLink) {
    attachBox
      .append("svg:image")
      .attr("x", xOffset + 10)
      .attr("y", yOffset + 50)
      .attr("width", 20)
      .attr("height", 24)
      .attr("xlink:href", "./img/decisionTree/download.png");
    attachBox
      .append("text")
      .attr("class", "attachText")
      .attr("x", xOffset + 40)
      .attr("y", yOffset + 70)
      .text("Download Attachment")
      .on("click", function() {
        clearAttachmentPopups();
        alert("Download Attachment");
      });
  }

  if (hasAttachment && !hasLink) {
    attachBox
      .append("svg:image")
      .attr("x", xOffset + 10)
      .attr("y", yOffset + 10)
      .attr("width", 20)
      .attr("height", 24)
      .attr("xlink:href", "./img/decisionTree/download.png");
    attachBox
      .append("text")
      .attr("class", "attachText")
      .attr("x", xOffset + 40)
      .attr("y", yOffset + 30)
      .text("Download Attachment")
      .on("click", function() {
        clearAttachmentPopups();
        alert("Download Attachment");
      });
  }
}

function wrap(text, width) {
  text.each(function() {
    let text = d3.select(this),
      words = text
        .text()
        .split(/\s+/)
        .reverse(),
      word,
      line = [],
      lineNumber = 0,
      lineHeight = 1.1, // ems
      x = text.attr("x"),
      y = text.attr("y"),
      dy = 1.1,
      tspan = text
        .text(null)
        .append("tspan")
        .attr("x", x)
        .attr("y", y)
        .attr("dy", dy + "em");
    while ((word = words.pop())) {
      line.push(word);
      tspan.text(line.join(" "));
      if (tspan.node().getComputedTextLength() > width) {
        line.pop();
        tspan.text(line.join(" "));
        line = [word];
        tspan = text
          .append("tspan")
          .attr("x", x)
          .attr("y", y)
          .attr("dy", ++lineNumber * lineHeight + dy + "em")
          .text(word);
      }
    }
  });
}

function findNodeById(currentNode, id) {
  var i, currentChild, result;

  if (id == currentNode.node_id) {
    return currentNode;
  } else {
    // Use a for loop instead of forEach to avoid nested functions
    // Otherwise "return" will not work properly
    for (i = 0; i < currentNode.children.length; i++) {
      currentChild = currentNode.children[i];

      // Search in the current child
      result = findNodeById(currentChild, id);

      // Return the result if the node has been found
      if (result !== false) {
        return result;
      }
    }

    // The node has not been found and we have no more options
    return false;
  }
}

function eraseNodesOfTree(tree) {
  if (!tree || tree.length === 0) {
    return;
  }
  for (var i = 0; i < tree.length; i++) {
    if (tree[i].children) {
      if (tree[i].isVisible) {
        d3.select("#" + CONTAINER_ID + " " + "#N" + tree[i].node_id).remove();
        d3.select("#" + CONTAINER_ID + " " + "#P" + tree[i].node_id).remove();
        d3.selectAll(
          "#" + CONTAINER_ID + " " + ".L" + tree[i].node_id
        ).remove();
        tree[i].isVisible = false;
      }
      eraseNodesOfTree(tree[i].children);
    }
  }
}

function eraseVisibleSubtreeOfChildren(tree, parentNodeId) {
  var parent = findNodeById(tree[0], parentNodeId);
  var children = parent.children;
  for (var i = 0; i < children.length; i++) {
    eraseNodesOfTree(children[i].children);
  }
}

function drawConnector(svg, p, c, nodeId) {
  //Draw Line AB
  drawLine(
    p.x + p.width + 5,
    p.y + p.height / 2,
    p.x + p.width + 50,
    p.y + p.height / 2,
    svg,
    nodeId
  );
  //Draw Line DE
  drawLine(c.x - 50, c.y + c.height / 2, c.x, c.y + c.height / 2, svg, nodeId);
  //Draw Line DB
  drawLine(
    c.x - 50,
    c.y + c.height / 2,
    p.x + p.width + 50,
    p.y + p.height / 2,
    svg,
    nodeId
  );
}

function drawLine(x1, y1, x2, y2, svg, nodeId) {
  svg
    .append("line")
    .attr("class", "L" + nodeId)
    .attr("x1", x1)
    .attr("y1", y1)
    .attr("x2", x2)
    .attr("y2", y2)
    .attr("stroke-width", 1);
}

function drawPointer(svg, n, nodeId) {
  var ptA = n.x + n.width - 0.45 + "," + (n.y + n.height / 2 - 5);
  var ptB = n.x + n.width + 5 + "," + (n.y + n.height / 2);
  var ptC = n.x + n.width - 0.45 + "," + (n.y + n.height / 2 + 5);

  svg
    .append("polyline")
    .attr("points", ptA + " " + ptB + " " + ptC)
    .attr("id", "P" + nodeId)
    .attr("class", "P");
}

function updateAllNodeColorsById(nodeId, tree) {
  //Apply the active class to the current node and pointer
  applyClassByNodeId(nodeId, "active");
  // color all ancestors
  var node = findNodeById(tree[0], nodeId);
  if (node.parent_id) {
    var parent = findNodeById(tree[0], node.parent_id);
    colorAllAncestors(tree, parent);
  }
}

function applyClassByNodeId(nodeId, className) {
  removeClassByNodeId(nodeId);
  d3.select("#" + CONTAINER_ID + " " + "#N" + nodeId + " rect").attr(
    "class",
    className
  );
  d3.select("#" + CONTAINER_ID + " " + "#P" + nodeId).attr("class", className);
}

function removeClassByNodeId(nodeId) {
  d3.select("#" + CONTAINER_ID + " " + "#N" + nodeId + " rect").attr(
    "class",
    null
  );
  d3.select("#" + CONTAINER_ID + " " + "#P" + nodeId).attr("class", null);
}

function colorAllAncestors(tree, node) {
  applyClassByNodeId(node.node_id, "onPath");
  if (node.parent_id) {
    var parent = findNodeById(tree[0], node.parent_id);
    colorAllAncestors(tree, parent);
  }
}

function clearAllPaths() {
  d3.selectAll("#" + CONTAINER_ID + " " + "rect").attr("class", null);
  d3.selectAll("#" + CONTAINER_ID + " " + "polyline").attr("class", null);
  d3.selectAll("#" + CONTAINER_ID + " " + ".indicators").remove();
  d3.selectAll("#" + CONTAINER_ID + " " + "rect").attr("class", "rect");
  d3.selectAll("#" + CONTAINER_ID + " " + "polyline").attr("class", "P");
}

function drawMetadataIndicators(svg, node, nodeBBox) {
  var indicatorsGroup = svg.append("g").attr("id", "I" + node.node_id);
  indicatorsGroup.attr("class", "indicators");
  var circleRadius = 5;
  var spacing = 7;
  var x = nodeBBox.x + nodeBBox.width;
  var y = nodeBBox.y - 2;
  var childrenCircle = indicatorsGroup
    .append("circle")
    .attr("r", circleRadius)
    .attr("class", "childsCircle");
  var totalCircle = indicatorsGroup
    .append("circle")
    .attr("r", circleRadius)
    .attr("class", "totalCircle");
  var levelCircle = indicatorsGroup
    .append("circle")
    .attr("r", circleRadius)
    .attr("class", "levelCircle");

  var childrenText = indicatorsGroup.append("text").text(node.children_ahead);
  var totalText = indicatorsGroup.append("text").text(node.children.length);
  var levelText = indicatorsGroup.append("text").text(node.level_ahead);

  // text lengths
  var childrenTextLength = childrenText.node().getComputedTextLength();
  var totalTextLength = totalText.node().getComputedTextLength();
  var levelTextLength = levelText.node().getComputedTextLength();

  levelText.attr("x", x - levelTextLength).attr("y", y);
  levelCircle.attr("cx", x - levelTextLength - spacing).attr("cy", y - 4);

  totalText
    .attr("x", x - levelTextLength - 2 * circleRadius - 2 * spacing)
    .attr("y", y);
  totalCircle
    .attr(
      "cx",
      x - levelTextLength - totalTextLength - 2 * circleRadius - 2 * spacing
    )
    .attr("cy", y - 4);

  childrenText
    .attr(
      "x",
      x -
        levelTextLength -
        totalTextLength -
        childrenTextLength -
        2 * circleRadius -
        3 * spacing
    )
    .attr("y", y);

  childrenCircle
    .attr(
      "cx",
      x -
        levelTextLength -
        totalTextLength -
        childrenTextLength -
        3 * circleRadius -
        3 * spacing
    )
    .attr("cy", y - 4);
}

function clearAttachmentPopups() {
  d3.selectAll("#" + CONTAINER_ID + " " + "#attachment").remove();
}

function resizeSvg(nodeGroup, svg) {
  var bBox = nodeGroup.node().getBBox();
  var svgBBox = svg.node().getBBox();
  var padding = 100;
  var xLength = 0;
  var yLength = 0;

  if (svgBBox.width <= bBox.x + bBox.width) {
    xLength = bBox.x + bBox.width - svgBBox.width;
  }
  if (svgBBox.height <= bBox.y + bBox.height) {
    yLength = bBox.y + bBox.height - svgBBox.height;
  }
  svg
    .attr("width", svgBBox.width + xLength + padding)
    .attr("height", svgBBox.height + yLength + padding);
}

function removeColorAndIndicatorsFromNodes() {
  d3.select(
    "#" + CONTAINER_ID + " " + "#N" + treeState[0].node_id + " rect"
  ).attr("class", null);
  d3.select("#" + CONTAINER_ID + " " + "#P" + treeState[0].node_id).attr(
    "class",
    null
  );
  d3.select(
    "#" + CONTAINER_ID + " " + "#N" + treeState[0].node_id + " rect"
  ).attr("class", "rect");
  d3.select("#" + CONTAINER_ID + " " + "#P" + treeState[0].node_id).attr(
    "class",
    "P"
  );
  d3.selectAll("#" + CONTAINER_ID + " " + ".indicators").remove();
}

function highlightNode(data, svgid) {
  var traversal = data.traversal;
  for (var i = 1; i < traversal.length; i++) {
    d3.select("#N" + traversal[i]).on("click")();
  }

  // var lastNode = findNodeById(treeState[0], traversal[traversal.length - 1]);
  // console.log(lastNode);
}

Buggy Style Application

Correct Class Application

...