以下の内容はhttps://ujimushisradjp.hatenablog.jp/entry/2024/11/29/012351より取得しました。


Plots.jlのGRバックエンドで横長の図を描いた時に凡例の線が長過ぎるのを調節する方法

仕事柄長い時系列のデータを複数要素表示するグラフをよく描くのですが,少し気になる挙動がありました。 凡例の中のがやや長過ぎる感じなのです。

次のようなコードで横長のグラフを描画してみます。

using Plots

plt = plot(sin; xlims=(0, 100pi), size=(1600, 200),
           legendfontfamily="ipag", label="線が長過ぎる")
savefig(plt, "plots-legend-line-too-long.png")

結果は次の通りです。やや線が長過ぎるような気がします。

で,GRバックエンド実装を見ると,この線の長さは グラフ(subplot)の横幅を基準として3/45の比で設定されているようでした。 なるほど。グラフを横長にすると間延びして見えるはずです。

で,この部分を変更できるように魔改造してみます。

次のコードを実装します。 これは,gr_legend_base_factor_per_ratioという参照型の変数を作って, グラフの横幅とlegendの基準長さとの比を可変にできるようにします。

using Plots

@eval Plots begin
    """
        gr_legend_base_factor_per_ratio

    legend描画時の横方向の基準(グラフの横幅/45)の比
    デフォルトでは45となっている。

    参照型なので, `gr_legend_base_factor_per_ratio[] = 100`
    のような形で変更すること
    """
    const gr_legend_base_factor_per_ratio = Ref(45.0)
    
    function gr_get_legend_geometry(vp, sp)
        vertical = (legend_column = sp[:legend_column]) == 1
        textw = texth = 0.0
        has_title = false
        nseries = 0
        if sp[:legend_position] !== :none
            GR.savestate()
            GR.selntran(0)
            GR.setcharup(0, 1)
            GR.setscale(0)
            ttl = sp[:legend_title]
            if (has_title = ttl !== nothing)
                gr_set_font(legendtitlefont(sp), sp)
                (l, r), (b, t) = extrema.(gr_inqtext(0, 0, string(ttl)))
                texth = t - b
                textw = r - l
            end
            gr_set_font(legendfont(sp), sp)
            for series in series_list(sp)
                should_add_to_legend(series) || continue
                (l, r), (b, t) = extrema.(gr_inqtext(0, 0, string(series[:label])))
                texth = max(texth, t - b)
                textw = max(textw, r - l)  # holds text width right now
                nseries += 1
            end
            GR.setscale(GR.OPTION_X_LOG)
            GR.selntran(1)
            GR.restorestate()
        end
        # deal with layout
        column_layout = if legend_column == -1
            (1, has_title + nseries)
        elseif legend_column > nseries && nseries != 0 # catch plot_title here
            @warn "n° of legend_column=$legend_column is larger than n° of series=$nseries"
            (1 + has_title, nseries)
        elseif legend_column == 0
            @warn "n° of legend_column=$legend_column. Assuming vertical layout."
            vertical = true
            (has_title + nseries, 1)
        else
            (ceil(Int64, nseries / legend_column) + has_title, legend_column)
        end
        #println(column_layout)

        base_factor = width(vp) /
            gr_legend_base_factor_per_ratio[]  # determines legend box base width (arbitrarily based on `width`)

        # legend box conventions ref(1)
        #  ______________________________
        # |<pad><span><space><text> <pad>|
        # |     ---o--       ⋅ y1        |
        # |__________________↑___________|
        #               (xpos,ypos)

        pad = 1base_factor  # legend padding
        span = 3base_factor  # horizontal span of the legend line: line x marker x line = 3base_factor
        space = 0.5base_factor  # white space between text and legend / markers

        # increment between each legend entry
        ekw = sp[:extra_kwargs]
        dy = texth * get(ekw, :legend_hfactor, 1)
        span_hspace = span + pad  # part of the horizontal increment
        dx = (textw + (vertical ? 0 : span_hspace)) * get(ekw, :legend_wfactor, 1)

        # This is to prevent that linestyle is obscured by large markers.
        # We are trying to get markers to not be larger than half the line length.
        # 1 / leg.dy translates base_factor to line length units (important in the context of size kwarg)
        # gr_legend_marker_to_line_factor is an empirical constant to translate between line length unit and marker size unit
        base_markersize = gr_legend_marker_to_line_factor[] * span / dy  # NOTE: arbitrarily based on horizontal measures !

        entries = has_title + nseries  # number of legend entries

        # NOTE: subtract `span_hspace`, since it joins labels in horizontal mode
        w = dx * column_layout[2] - space - !vertical * span_hspace
        h = dy * column_layout[1]

        (
            yoffset = height(vp) / 30, xoffset = width(vp) / 30,
            base_markersize, base_factor, has_title, vertical,
            entries, column_layout, space,
            texth, textw, span, pad, dy, dx, w, h,
        )
    end
end

上のコードを実行してから,次のように実行します。

# 凡例の基準長さをグラフ横幅の1/150にする
Plots.gr_legend_base_factor_per_ratio[] = 150.0

plt = plot(sin; xlims=(0, 100pi), size=(1600, 200),
           legendfontfamily="ipag", label="線を調整")
savefig(plt, "plots-legend-line-short.png")

結果は次の通りです。 凡例の線が短くなっていることが分かると思います。

このように内部の実装を覗いて自分だけの魔改造をするのも楽しいものです。

最近はプルリクで提案するのが普通なんでしょうが,この辺は好みなので 採用されないことも多いので,本体のライブラリを変更せずに 改造できるのは個人的には非常に重宝しています。

注意点

対応しているのはPlots.jlのバージョン1.xです。 いつかリリースされるバージョン2からはPlots.jlモジュールの構造が変わるので上記のコードでは動かないので注意して下さい。




以上の内容はhttps://ujimushisradjp.hatenablog.jp/entry/2024/11/29/012351より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14