# PlotlyJS function to plot with up to 4 Y-axes

**URL:** <https://discourse.julialang.org/t/plotlyjs-function-to-plot-with-up-to-4-y-axes/136184>\
**Category:** General Usage\
**Tags:** plotlyjs, multiple-axes\
**Created:** [March 13, 2026, 1:42pm UTC](https://discourse.julialang.org/t/plotlyjs-function-to-plot-with-up-to-4-y-axes/136184 "2026-03-13T13:42:10Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![ellocco](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ellocco/32/31331_2.png) [@ellocco](https://discourse.julialang.org/u/ellocco)\
**Post date:** [March 13, 2026, 1:42pm UTC](https://discourse.julialang.org/t/plotlyjs-function-to-plot-with-up-to-4-y-axes/136184/1 "2026-03-13T13:42:10Z")

</div>

Below an idea for a PlotlyJS-based user defined plotting function.  
The function can be called with an x-vector and an y-vector/-array.  
And it can handle up to 4-y-axes as well.  
I am happy about comments.

```julia-auto
"""

    priv_plot_data(x_vec, y_array, ...) or
    priv_plot_data([], y_array, ...)

        Gives back a PlotlyJS-Print-Object.

# Inputs:
* `x_vec`: x-values
* `y_data`: y-data matrix (trace data points)
* `trace_labels`: trace lables
* `title_and_x_axis_label`: two element string vector
* `y_axis_labels`: coloured trace specific labels on each axis
* `axes_bindings`: bind a trace to one of the y-axes

# Named Inputs (optional):
* `color_palette`: your own colorway
* `axis_positions`: seven-element vector to define the horizontal location of the y-axes
* `in_xDBG`: variable name or label for exspression to be investigated

# Content of seven-element `axis_positions`:
| | | |
|:----------------------------|:-----------------------|------------------------------:|
| __[1]__ domain\\_start | __[2]__ domain\\_end | |
| __[3]__ pos\\_y3\\_left | __[4]__ pos\\_y4_left | (-1 → that axis goes right) |
| __[5]__ pos\\_right1 | __[6]__ pos\\_right2 | (-1 → slot unused) |
| __[7]__ pos\\_right3 | | (-1 → slot unused) |

# Output:
    PlotlyJS-Print-Object

# Current Developer:
    Stefan Pofahl (12-03-2026)

"""
function priv_plot_traces(in_x_vec::AbstractVector, in_y_data::Union{AbstractVector, AbstractMatrix},
        in_trace_labels::AbstractVector=[""], in_title_and_x_axis_label::AbstractVector=[""],
        in_y_axis_labels::AbstractVector=[""], in_axes_bindings::AbstractVector=[];
        color_palette::AbstractVector=[""], in_xDBG::Bool=false,
        axis_positions::AbstractVector{<:Real}=Float64[])
    #: --- ToDo --------------------------------------------------------------------------------
    #: optioonal parameters:
    #: - xMarkerON (vector of int, 0= false, 1= true)
    #: - define the position of the legend (can also be used to switch off the legend)
    #: - suffix, affix for y-Axis-Labels, e.g.: "Forces: " ... " / kN"
    #: -----------------------------------------------------------------------------------------
    data_pts = size(in_y_data, 1)
    traces_count = size(in_y_data, 2)
    if isempty(in_x_vec)
        in_x_vec = collect(1:data_pts)
    end
    if isempty(color_palette) || color_palette == [""]
        color_palette::Vector{String} = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd",
                         "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"]
    end
    if isempty(in_title_and_x_axis_label[1]); in_title_and_x_axis_label = ["", "x-axis"]; end;
    yaxis_list = ["y", "y2", "y3", "y4"]
    yaxis_keys = [:yaxis2, :yaxis3, :yaxis4]
    max_y_axes = length(yaxis_list)
    #: ---
    if isempty(in_trace_labels[1])
        in_trace_labels = Vector{String}(undef, traces_count); fill!(in_trace_labels, "")
        for i = 1:traces_count
            in_trace_labels[i] = string("y", i);
        end
    end;
    #: ---
    if isempty(in_y_axis_labels[1])
        in_y_axis_labels = Vector{String}(undef, traces_count); fill!(in_y_axis_labels, "")
        for i = 1:traces_count
            in_y_axis_labels[i] = string("tr", i);
        end
    end;
    if isempty(in_axes_bindings); in_axes_bindings = ones(Int, traces_count); end;

    # ── Input validation ──────────────────────────────────────────────────────
    length(in_title_and_x_axis_label) != 2 && error("in_title_and_x_axis_label needs exactly 2 elements [title, x-label]")
    data_pts != size(in_y_data, 1) && error("Y-Data rows $(size(in_y_data,1)) ≠ x-vector length $data_pts")
    traces_count != length(in_trace_labels) && error("y_labels length $(length(in_trace_labels)) ≠ data columns $traces_count")
    length(in_y_axis_labels) != traces_count && error("in_y_axis_labels must have length $traces_count (one per trace)")
    traces_count != length(in_axes_bindings) && error("axes_bindings length $(length(in_axes_bindings)) ≠ data columns $traces_count")
    !isempty(axis_positions) && length(axis_positions) != 7 && error("axis_positions must have exactly 7 elements")

    in_axes_bindings = Int.(round.(in_axes_bindings))
    used_axes = sort(unique(in_axes_bindings))
    right_spacing = 0.12 # in case axis_positions is not specified

    # ── Decode geometry from axis_positions (or auto-compute) ─────────────────
    #
    # axis_positions layout:
    # [1] domain_start [2] domain_end
    # [3] pos_y3_left [4] pos_y4_left (-1 → that axis goes right)
    # [5] pos_right1 [6] pos_right2 [7] pos_right3 (-1 → slot unused)
    #
    # Decision rule:
    # axis_positions[3] == -1 → all secondary axes on right (≙ case 1)
    # axis_positions[3] != -1 → y3 on left, rest on right (≙ case 2)
    # axis_positions[4] != -1 → additionally y4 on left
    # ──────────────────────────────────────────────────────────────────────────

    left_axes_ids = [1] # y1 always primary-left
    right_axes_ids = Int[]
    left_positions = Dict{Int, Float64}()
    right_positions = Dict{Int, Float64}()

    if isempty(axis_positions)
        # ── Auto-compute: all secondary axes right (classic case 1) ──────────
        if length(used_axes) == 2
            right_axes_ids = filter(a -> a > 1, used_axes)
            n_right = length(right_axes_ids)
            x_domain_start = 0.0
            x_domain_end = max(0.99, 1.0 - n_right * right_spacing)
            right_positions = Dict(ax => 1.0 - (i - 1) * right_spacing
                                for (i, ax) in enumerate(right_axes_ids))
        elseif length(used_axes) == 3
            right_axes_ids = filter(a -> a > 1, used_axes)
            n_right = length(right_axes_ids)
            x_domain_start = 0.0
            x_domain_end = max(0.65, 1.0 - n_right * right_spacing)
            right_positions = Dict(ax => 1.0 - (i - 1) * right_spacing
                                for (i, ax) in enumerate(right_axes_ids))
        else
            right_axes_ids = filter(a -> a > 1, used_axes)
            n_right = length(right_axes_ids)
            x_domain_start = 0.0
            x_domain_end = max(0.65, 1.0 - n_right * right_spacing)
            right_positions = Dict(ax => 1.0 - (i - 1) * right_spacing
                                for (i, ax) in enumerate(right_axes_ids))
        end

    else
        # ── Explicit positions from vector ────────────────────────────────────
        x_domain_start = axis_positions[1]
        x_domain_end = axis_positions[2]
        pos_left_slots = axis_positions[3:4] # indexed by: y3→slot1, y4→slot2
        pos_right_slots = axis_positions[5:7]

        left_slot_map = Dict(3 => 1, 4 => 2) # which left slot each axis uses
        right_slot_idx = 1 # next available right slot

        for ax in filter(a -> a > 1, used_axes)
            # Check if this axis has a left-slot assignment
            slot = get(left_slot_map, ax, 0)
            if slot > 0 && pos_left_slots[slot] != -1.0
                push!(left_axes_ids, ax)
                left_positions[ax] = pos_left_slots[slot]
            else
                # Validate that a right slot is available
                while right_slot_idx <= 3 && pos_right_slots[right_slot_idx] == -1.0
                    right_slot_idx += 1
                end
                right_slot_idx > 3 && error("Not enough right slots in axis_positions for axis y$ax")
                push!(right_axes_ids, ax)
                right_positions[ax] = pos_right_slots[right_slot_idx]
                right_slot_idx += 1
            end
        end
        n_right = length(right_axes_ids)
    end

    # ── Build colored y-axis title parts ─────────────────────────────────────
    y_title_parts = Matrix{String}(undef, traces_count, max_y_axes)
    fill!(y_title_parts, "")
    vector_of_traces = PlotlyJS.GenericTrace[]

    for i in 1:traces_count
        color_i = color_palette[mod1(i, length(color_palette))]
        yAxesNr = in_axes_bindings[i]

        y_title_parts[i, yAxesNr] = "<span style='color:$color_i'>$(in_y_axis_labels[i])</span>"

        push!(vector_of_traces, PlotlyJS.scatter(;
            x = in_x_vec,
            y = in_y_data[:, i],
            mode = "lines+markers",
            name = in_trace_labels[i],
            line = PlotlyJS.attr(color = color_i),
            marker = PlotlyJS.attr(color = color_i),
            yaxis = yaxis_list[yAxesNr]
        ))
    end

    Y_AxesLabelVector = [join(filter(!isempty, col), " ") for col in eachcol(y_title_parts)]

    # ── Build layout kwargs dynamically ──────────────────────────────────────
    layout_kwargs = Dict{Symbol, Any}(
        :title => in_title_and_x_axis_label[1],
        :legend => PlotlyJS.attr(x = 0.01, y = 0.99),
        :width => 1000,
        :height => 600,
        :xaxis => PlotlyJS.attr(
            title = in_title_and_x_axis_label[2],
            showgrid = true,
            zeroline = true,
            domain = [x_domain_start, x_domain_end]
        )
    )

    layout_kwargs[:yaxis] = PlotlyJS.attr(
        title = Y_AxesLabelVector[1],
        titlefont = PlotlyJS.attr(size = 14),
        showgrid = true,
        zeroline = true,
        side = "left"
    )

    for ax in filter(a -> a != 1, left_axes_ids)
        layout_kwargs[yaxis_keys[ax - 1]] = PlotlyJS.attr(
            title = Y_AxesLabelVector[ax],
            titlefont = PlotlyJS.attr(size = 14),
            showgrid = false,
            zeroline = false,
            overlaying = "y",
            side = "left",
            position = left_positions[ax]
        )
    end

    for ax in right_axes_ids
        layout_kwargs[yaxis_keys[ax - 1]] = PlotlyJS.attr(
            title = Y_AxesLabelVector[ax],
            titlefont = PlotlyJS.attr(size = 14),
            showgrid = false,
            zeroline = false,
            overlaying = "y",
            side = "right",
            position = right_positions[ax]
        )
    end

    layout = PlotlyJS.Layout(; layout_kwargs...)
    plt = PlotlyJS.plot(vector_of_traces, layout)
    if in_xDBG
        return plt, n_right, x_domain_end, left_positions, right_positions, left_axes_ids, right_axes_ids
    else
        return plt
    end
end

```
