private getDefaultAnchor()

in src/app/views/panels/link_creator.tsx [169:479]


  private getDefaultAnchor(
    manager: Prototypes.ChartStateManager,
    linkMode: string,
    cs: Graphics.CoordinateSystem,
    glyph1: Specification.Glyph,
    glyphState1: Specification.GlyphState,
    // eslint-disable-next-line
    glyph2: Specification.Glyph,
    glyphState2: Specification.GlyphState
  ): {
    linkType: Prototypes.Links.LinkType;
    interpolationType: Prototypes.Links.InterpolationType;
    anchor1: Specification.Types.LinkAnchorPoint[];
    anchor2: Specification.Types.LinkAnchorPoint[];
    color: Specification.Mapping;
    opacity: Specification.Mapping;
  } {
    // Default color and opacity
    let color: Specification.Mapping;
    let opacity: Specification.Mapping;
    switch (this.state.linkType) {
      case "line":
        {
          color = {
            type: MappingType.value,
            value: { r: 0, g: 0, b: 0 },
          } as Specification.ValueMapping;
          opacity = {
            type: MappingType.value,
            value: 1,
          } as Specification.ValueMapping;
        }
        break;
      case "band":
        {
          color = {
            type: MappingType.value,
            value: { r: 0, g: 0, b: 0 },
          } as Specification.ValueMapping;
          opacity = {
            type: MappingType.value,
            value: 0.5,
          } as Specification.ValueMapping;
        }
        break;
    }

    // Get anchor candidates
    let candidates1: Prototypes.LinkAnchor.Description[] = [];
    let candidates2: Prototypes.LinkAnchor.Description[] = [];
    for (const mark of glyphState1.marks) {
      const c = manager.getMarkClass(mark);
      if (c.getLinkAnchors) {
        candidates1 = candidates1.concat(c.getLinkAnchors("begin"));
      }
    }
    for (const mark of glyphState2.marks) {
      const c = manager.getMarkClass(mark);
      if (c.getLinkAnchors) {
        candidates2 = candidates2.concat(c.getLinkAnchors("end"));
      }
    }
    // Filter based on link type
    switch (this.state.linkType) {
      case "line":
        {
          candidates1 = candidates1.filter((x) => x.points.length == 1);
          candidates2 = candidates2.filter((x) => x.points.length == 1);
        }
        break;
      case "band": {
        candidates1 = candidates1.filter((x) => x.points.length == 2);
        candidates2 = candidates2.filter((x) => x.points.length == 2);
      }
    }

    const glyphAttributes1 = glyphState1.attributes as Prototypes.Glyphs.RectangleGlyphAttributes;
    const glyphAttributes2 = glyphState2.attributes as Prototypes.Glyphs.RectangleGlyphAttributes;

    const determineRelationship = (
      a1: number,
      a2: number,
      b1: number,
      b2: number
    ): "before" | "overlap" | "after" => {
      // Make sure order is correct
      [a1, a2] = [Math.min(a1, a2), Math.max(a1, a2)];
      [b1, b2] = [Math.min(b1, b2), Math.max(b1, b2)];
      if (a2 <= b1) {
        return "after";
      } else if (a1 >= b2) {
        return "before";
      } else {
        return "overlap";
      }
    };
    // Determine relative position
    const xRelationship = determineRelationship(
      glyphAttributes1.x1,
      glyphAttributes1.x2,
      glyphAttributes2.x1,
      glyphAttributes2.x2
    );
    const yRelationship = determineRelationship(
      glyphAttributes1.y1,
      glyphAttributes1.y2,
      glyphAttributes2.y1,
      glyphAttributes2.y2
    );

    const meanPoint = (points: Point[]): Point => {
      let x = 0,
        y = 0;
      for (const pt of points) {
        x += pt.x;
        y += pt.y;
      }
      return {
        x: x / points.length,
        y: y / points.length,
      };
    };

    let c1: Prototypes.LinkAnchor.Description = null,
      c2: Prototypes.LinkAnchor.Description = null;
    if (xRelationship == "after") {
      if (linkMode == "link-table") {
        c1 = candidates1[argMin(candidates1, (c) => meanPoint(c.points).y)];
        c2 = candidates2[argMin(candidates2, (c) => meanPoint(c.points).y)];
      } else {
        c1 = candidates1[argMax(candidates1, (c) => meanPoint(c.points).x)];
        c2 = candidates2[argMin(candidates2, (c) => meanPoint(c.points).x)];
      }
    } else if (xRelationship == "before") {
      if (linkMode == "link-table") {
        c1 = candidates1[argMin(candidates1, (c) => meanPoint(c.points).y)];
        c2 = candidates2[argMin(candidates2, (c) => meanPoint(c.points).y)];
      } else {
        c1 = candidates1[argMin(candidates1, (c) => meanPoint(c.points).x)];
        c2 = candidates2[argMax(candidates2, (c) => meanPoint(c.points).x)];
      }
    } else {
      if (yRelationship == "after") {
        if (linkMode == "link-table") {
          c1 = candidates1[argMin(candidates1, (c) => meanPoint(c.points).x)];
          c2 = candidates2[argMin(candidates2, (c) => meanPoint(c.points).x)];
        } else {
          c1 = candidates1[argMax(candidates1, (c) => meanPoint(c.points).y)];
          c2 = candidates2[argMin(candidates2, (c) => meanPoint(c.points).y)];
        }
      } else if (yRelationship == "before") {
        if (linkMode == "link-table") {
          c1 = candidates1[argMin(candidates1, (c) => meanPoint(c.points).x)];
          c2 = candidates2[argMin(candidates2, (c) => meanPoint(c.points).x)];
        } else {
          c1 = candidates1[argMin(candidates1, (c) => meanPoint(c.points).y)];
          c2 = candidates2[argMax(candidates2, (c) => meanPoint(c.points).y)];
        }
      } else {
        c1 =
          candidates1[
            argMin(candidates1, (c) => Math.abs(meanPoint(c.points).y))
          ];
        c2 =
          candidates2[
            argMin(candidates2, (c) => Math.abs(meanPoint(c.points).y))
          ];
      }
    }

    switch (this.state.linkType) {
      case "line":
        {
          if (c1 == null) {
            c1 = {
              element: null,
              points: [
                {
                  xAttribute: "icx",
                  yAttribute: "icy",
                  x: 0,
                  y: 0,
                  direction: { x: 0, y: 0 },
                },
              ],
            };
          }
          if (c2 == null) {
            c2 = {
              element: null,
              points: [
                {
                  xAttribute: "icx",
                  yAttribute: "icy",
                  x: 0,
                  y: 0,
                  direction: { x: 0, y: 0 },
                },
              ],
            };
          }
        }
        break;
      case "band":
        {
          if (c1 == null) {
            c1 = {
              element: null,
              points: [
                {
                  xAttribute: "icx",
                  yAttribute: "iy1",
                  x: 0,
                  y: 0,
                  direction: { x: 1, y: 0 },
                },
                {
                  xAttribute: "icx",
                  yAttribute: "iy2",
                  x: 0,
                  y: 0,
                  direction: { x: 1, y: 0 },
                },
              ],
            };
          }
          if (c2 == null) {
            c2 = {
              element: null,
              points: [
                {
                  xAttribute: "icx",
                  yAttribute: "iy1",
                  x: 0,
                  y: 0,
                  direction: { x: -1, y: 0 },
                },
                {
                  xAttribute: "icx",
                  yAttribute: "iy2",
                  x: 0,
                  y: 0,
                  direction: { x: -1, y: 0 },
                },
              ],
            };
          }
        }
        break;
    }

    const anchor1 = c1.points.map((pt) => {
      return {
        x: { element: c1.element, attribute: pt.xAttribute },
        y: { element: c1.element, attribute: pt.yAttribute },
        direction: pt.direction,
      } as Specification.Types.LinkAnchorPoint;
    });

    const anchor2 = c2.points.map((pt) => {
      return {
        x: { element: c2.element, attribute: pt.xAttribute },
        y: { element: c2.element, attribute: pt.yAttribute },
        direction: pt.direction,
      } as Specification.Types.LinkAnchorPoint;
    });

    if (linkMode != "link-table") {
      if (c1.element != null) {
        const element1 = getById(glyph1.marks, c1.element);
        switch (element1.classID) {
          case "mark.symbol":
            {
              if (element1.mappings.fill != null) {
                color = element1.mappings.fill;
              }
            }
            break;
          case "mark.rect":
            {
              if (element1.mappings.fill != null) {
                color = element1.mappings.fill;
              }
            }
            break;
        }
      }
    }

    let interpolationType: Prototypes.Links.InterpolationType = "line";
    if (cs instanceof Graphics.PolarCoordinates) {
      interpolationType = "bezier";
    } else {
      interpolationType = "line";
    }
    // If directions are the same direction, switch line to circle.
    if (Geometry.vectorDot(anchor1[0].direction, anchor2[0].direction) > 0) {
      if (interpolationType == "line") {
        interpolationType = "circle";
      }
    }

    return {
      linkType: this.state.linkType,
      interpolationType,
      anchor1,
      anchor2,
      color,
      opacity,
    };
  }