Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Examples drawing arrows using trails #122

Open
jerinphilip opened this issue Aug 23, 2023 · 9 comments
Open

Examples drawing arrows using trails #122

jerinphilip opened this issue Aug 23, 2023 · 9 comments

Comments

@jerinphilip
Copy link

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

@danoneata
Copy link
Collaborator

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

@jerinphilip
Copy link
Author

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

@danoneata
Copy link
Collaborator

danoneata commented Aug 25, 2023

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.

@jerinphilip
Copy link
Author

jerinphilip commented Aug 27, 2023

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.

@danoneata
Copy link
Collaborator

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

@danoneata
Copy link
Collaborator

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?

@jerinphilip
Copy link
Author

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

@danoneata
Copy link
Collaborator

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?

@jerinphilip
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants