chalk icon indicating copy to clipboard operation
chalk copied to clipboard

Examples drawing arrows using trails

Open jerinphilip opened this issue 2 years ago • 9 comments

There are currently no examples of using arrows with trails in ./examples/arrows.py, so I'm working off bits and pieces here and in haskell diagrams. I'm not very strong with haskell, arrow.html#lengths-and-gaps looks similar to my problem but I'm not sure how to proceed.

I'm trying to get A to connect to B, using arrows and trails.

image

code (click to expand)
from PIL import Image as PILImage
from chalk import *
from colour import Color
from copy import deepcopy
from argparse import ArgumentParser


EM = 12
LINE_WIDTH = 0.05

black = Color("#000000")
white = Color("#ffffff")
grey = Color("#cccccc")
green = Color("#00ff00")


def Block(
    label: str,
    width: int = 5 * EM,
    height: int = 2 * EM,
    stroke: Color = black,
    fill: Color = white,
):
    radius = min(width, height) / 20
    font_size = min(width, height) / 2
    container = (
        rectangle(width, height, radius)
        .line_color(stroke)
        .fill_color(fill)
        .line_width(LINE_WIDTH)
    ).center_xy()

    bounding_rect = container.scale(0.9).center_xy()
    render_label = (
        text(label, font_size)
        .line_color(black)
        .line_width(0)
        .fill_color(black)
        .with_envelope(bounding_rect)
    ).translate(0, height / 16)

    return container + render_label


if __name__ == "__main__":
    parser = ArgumentParser()
    parser.add_argument("--path", type=str, required=True)
    args = parser.parse_args()

    A_block = Block("A").named("A")
    node = square(1).fill_color(None).line_color(None)
    A = vcat([node.named("a"), A_block]).center_xy()

    B_block = Block("B").named("B")
    B = hcat([node.named("b"), B_block]).center_xy()

    diagram = hcat([A, B], 2 * EM)
    trail = Trail.from_offsets(
        [
            V2(0, -EM),
            V2(EM, 0),
            V2(0, EM),
            V2(EM, 0),
        ]
    )

    arrow1_opts = ArrowOpts(
        **{
            "head_pad": 0,
            "tail_pad": 0,
            "trail": trail,
            "arc_height": 0,
            "shaft_style": Style.empty().line_color(grey),
        }
    )

    arrow2_opts = ArrowOpts(
        **{
            "head_pad": 0,
            "tail_pad": 0,
            "trail": trail,
            "arc_height": 0,
            "shaft_style": Style.empty().line_color(green),
        }
    )

    arrow_hidden_from_centers = diagram.connect("A", "B", arrow1_opts)
    arrow_outside = diagram.connect("a", "b", arrow2_opts)
    diagram = arrow_hidden_from_centers + diagram + arrow_outside

    diagram.render_svg(args.path, height=512)

As visible from the picture, I'm not having much luck with this. I tried some rotation of the arrow (outside) but that ends up weird. I'm assuming the trail is placed on the line between the nodes a and b. Placing the arrow (inside, but atop) through A and B centers get me the desired result (grey), but in this case I find it difficult to place heads (dart) and tails without configuring pad.

The larger diagram I'm trying to solve the above for is below (trying to connect an encoder and decoder), and the endpoints are not parallel to horizontal:

translation (click to expand)

image

jerinphilip avatar Aug 23 '23 14:08 jerinphilip

Hello Jerin! Thanks for the report! I'll try to have a look at the issue these days.

danoneata avatar Aug 24 '23 08:08 danoneata

Hi, I have worked around this for the time being by just manually finding locations and adding an elbow connector myself.

snippet
eout = diagram.get_subdiagram("encoder_out").get_location()
d0in = diagram.get_subdiagram("decoder_0_in").get_location()
print(eout)
print(d0in)

# Create an elbow connector.
dx = -1 * (eout.x - d0in.x)
dy = -1 * (eout.y - d0in.y)

up = 4
p0 = V2(0, 0)
p1 = V2(0, -1 * up)
p2 = V2(dx / 2, 0)
p3 = V2(0, (dy + up))
p4 = V2(dx / 2, 0)
# print(eout + p1 + p2 + p3 + p4, d0in)
# assert p4 == d0in

elbow_connector = trail.stroke().line_color(grey).translate(eout.x, eout.y)
diagram = elbow_connector + diagram
render

image

jerinphilip avatar Aug 24 '23 16:08 jerinphilip

Cool! I think your solution provides a good compromise for the current situation.

I thought about this issue as well, but I have failed to come up with a better answer. As you've noticed, the reason of the observed behavior is that the arrow's trail is relative to its orientation (the start-point to end-point vector). I've also tried getting some inspiration from the Haskell example that you've mentioned, but I had trouble understanding it.

Maybe flowcharts should use a different API, for example, something similar to the one in TikZ? Let me know if you have any suggestions on this matter.

danoneata avatar Aug 25 '23 13:08 danoneata

While trying out a few more diagrams - I get a feeling a connect_elbow(source, target, *args), will be enough to create a few basic elbow connectors (⤷⤴⤵⤶ and rotations, reflections). I'm not sure what args look like at this point, I think it could be a V2(dx, 0) or V2(0, dy) which encodes a start direction, used in conjunction with boundary_from(...) to get boundary, and followed by automatic work out of a path constrained to use horizontal or vertical movements to generate an elbow connector.

While I'm familiar with Tikz, I have not used it enough to form an opinion. My preferred choices for diagrams at the moment are inkscape > excalidraw > ppt-software. I'm currently trying chalk to replace this with a chalk + finishing touch-ups by inkscape workflow.

jerinphilip avatar Aug 27 '23 18:08 jerinphilip

Thanks for the feedback @jerinphilip! I'm currently on vacation, but I'll try to implement your suggestion when I get back.

danoneata avatar Sep 01 '23 06:09 danoneata

I've added a connect_outside_elbow function here (on the 122-elbow-connections branch). The function supports two types of connections: "hv" (horizontal-then-vertical connection) or "vh" (vertical-then-horizontal connection).

An example would be:

from colour import Color
from chalk import *
from chalk.arrow import connect_outside_elbow

color = Color("pink")

def make_dia():
    c1 = circle(0.75).fill_color(color).named("src") + text("src", 0.7)
    c2 = circle(0.75).fill_color(color).named("tgt") + text("tgt", 0.7)
    return c1 + c2.translate(3, 3)

dia1 = make_dia()
dia1 = connect_outside_elbow(dia1, "src", "tgt", "hv")

dia2 = make_dia()
dia2 = connect_outside_elbow(dia2, "src", "tgt", "vh")

dia = hcat([dia1, dia2], sep=2)

path = "examples/output/connect_elbow.svg"
dia.render_svg(path, height=256)

yielding

Screenshot 2023-09-18 at 10 59 57

Is this similar to what you envisioned?

danoneata avatar Sep 18 '23 08:09 danoneata

I get the following directly using your code:

image

Adding .line_width(0) to text gives me this:

image

I think this is an entirely different bug?

I'm happy with the results I can achieve with this convenience.

elbow.py
from colour import Color
from chalk import *
from chalk.arrow import connect_outside_elbow
from .cli import basic_parser

if __name__ == "__main__":
    parser = basic_parser()
    args = parser.parse_args()
    color = Color("pink")

    def node(label):
        c = circle(0.75).fill_color(color).named(label) + text(label, 0.7).line_width(0)
        return c

    def make_diagram():
        points = [V2(3, 0), V2(0, 3), V2(-3, 0), V2(0, -3)]
        diagram = empty()
        for idx, point in enumerate(points):
            diagram = diagram + node(f"c{idx}").translate(point.x, point.y)

        return diagram

    clockwise = make_diagram()
    for idx in range(1, 4):
        direction = "hv" if idx % 2 == 0 else "vh"
        clockwise = connect_outside_elbow(clockwise, f"c{idx-1}", f"c{idx}", direction)

    direction = "hv"
    clockwise = connect_outside_elbow(clockwise, f"c3", f"c0", direction)

    counterclockwise = make_diagram()
    for idy in range(1, 4):
        idx = 4 - idy
        direction = "hv" if idx % 2 == 1 else "vh"
        counterclockwise = connect_outside_elbow(
            counterclockwise, f"c{idx}", f"c{idx-1}", direction
        )

    direction = "vh"
    counterclockwise = connect_outside_elbow(counterclockwise, f"c0", f"c3", direction)
    # dia2 = make_diagram()
    # dia2 = connect_outside_elbow(dia2, "src", "tgt", "vh")

    # dia = hcat([dia1, dia2], sep=2)
    diagram = hcat([clockwise, counterclockwise], sep=2)

    diagram.render_svg(args.path, height=256)

image

jerinphilip avatar Sep 21 '23 17:09 jerinphilip

Hmm... the first rendering looks unexpected. In this Colab it looks fine. What tool do you use to open and view the SVG? Can it be related to this?

danoneata avatar Sep 24 '23 21:09 danoneata

I'm running ArchLinux, I suspect this could be due to a font difference (hence line_width(0) helping out). Colab runs in a google modified Ubuntu environment, so the difference could just be that. If we increase line-width in the colab, we can notice getting similar artifacts. Are there any settings to control font in the Cairo backend exposed?

I'm using Eye of Gnome (eog), the default image-viewer on Gnome.

jerinphilip avatar Sep 25 '23 00:09 jerinphilip