Skip to content
Workflow: A Scatter with Marginals

Workflow: A Scatter with Marginals

Use this when: You need to show two variables’ relationship and each one’s own distribution in one figure, with the marginal histograms lined up on the scatter’s axes.

Workflow: A Scatter with Marginals

Usage

from dataviz import jointplot, save


def main() raises:
    # A correlated pair from a fixed linear congruential sequence.
    var height_cm = List[Float64]()
    var weight_kg = List[Float64]()
    var s = 20260919
    for _ in range(300):
        s = (s * 1103515245 + 12345) % 2147483648
        var h = 150.0 + Float64(s % 4000) / 100.0
        s = (s * 1103515245 + 12345) % 2147483648
        var noise = Float64(s % 2000) / 100.0 - 10.0
        height_cm.append(h)
        weight_kg.append(0.9 * (h - 100.0) + noise)

    var path = "docs/src/examples/out_workflow_scatter_marginals.svg"
    var figure = jointplot(
        height_cm,
        weight_kg,
        title="Height and weight",
        x_title="Height (cm)",
        y_title="Weight (kg)",
    )
    save(figure, path)

    # Read the x tick labels back: middle-anchored text, one row per
    # panel. The top marginal's row is the highest, the scatter's the
    # lowest; a label in both must sit at the same x.
    var f = open(path, "r")
    var svg = f.read()
    f.close()
    var xs = List[Float64]()
    var ys = List[Float64]()
    var texts = List[String]()
    var at = 0
    while True:
        var open_at = svg.find("<text ", at)
        if open_at < 0:
            break
        var tag_end = svg.find(">", open_at)
        var tag = String(svg[byte=open_at:tag_end])
        var close_at = svg.find("</text>", tag_end)
        at = close_at
        if 'text-anchor="middle"' not in tag or "rotate(" in tag:
            continue
        var x0 = tag.find('x="') + 3
        var y0 = tag.find('y="') + 3
        xs.append(Float64(String(tag[byte = x0 : tag.find('"', x0)])))
        ys.append(Float64(String(tag[byte = y0 : tag.find('"', y0)])))
        texts.append(String(svg[byte = tag_end + 1 : close_at]))
    var top_row = 1.0e9
    var bottom_row = -1.0e9
    for i in range(len(ys)):
        var numeric = True
        for b in texts[i].as_bytes():
            # Digits, a decimal point or a minus sign.
            if not ((b >= 48 and b <= 57) or b == 46 or b == 45):
                numeric = False
        if not numeric or texts[i].byte_length() == 0:
            continue
        top_row = min(top_row, ys[i])
        bottom_row = max(bottom_row, ys[i])
    if not (top_row < bottom_row):
        raise Error(
            "expected x tick labels under both the marginal and the scatter"
        )
    var matched = 0
    for i in range(len(ys)):
        if ys[i] != top_row:
            continue
        for j in range(len(ys)):
            if ys[j] == bottom_row and texts[j] == texts[i]:
                matched += 1
                if abs(xs[i] - xs[j]) > 0.5:
                    raise Error(
                        "tick "
                        + texts[i]
                        + " is at x="
                        + String(xs[i])
                        + " above but x="
                        + String(xs[j])
                        + " below: the marginal is not aligned"
                    )
    if matched == 0:
        raise Error(
            "found no tick label shared by the marginal and the scatter"
        )

Related

Related recipes: Workflow: Comparing Distributions · Workflow: A Publication Figure

Relevant API: Data shapes