*! version 2.0.0  14jun2026  Ben Jann

program _mklegend, rclass
    version 14
    _on_colon_parse `0'
    local 0 `"`s(before)'"'

    // parse s(after)
    mata: keys_expand() // returns key_n, key_#

    // parse [graphname] [numlist]
    _parse comma subgr 0 : 0
    gettoken graph : subgr
    capt confirm name `graph'
    if _rc==0 gettoken graph subgr : subgr
    else local graph
    if `"`subgr'"'!="" {
        numlist `"`subgr'"', integer range(>=1)
        local subgr `r(numlist)'
    }

    // parse options
    syntax [, nodraw Margin(passthru) /*
        */ FRame FRame2(str) lskip(real 1.5)/*
        */ y(real 96) x(real 2) h(real 5) w(real 5)/*
        */ ty(real 0) tx(numlist max=1) tw(real 20) Text(str)/*
        */ PSTYle(passthru) * ]
    mata: extract_opts(("Y", "X", "H", "W", "TY", "TX", "TW"))
    foreach opt in Y X H W TY TX TW {
        if "``opt''"!="" numlist `"``opt''"', max(1)
    }
    foreach opt in Y X H W tx TY TX TW {
        if "``opt''"=="" local `opt' .
    }
    parse_topts, `text' // returns place, just, topts
    if `"`frame'`frame2'"'!="" {
        local frame frame
        parse_frame, `frame2'
    }

    // get limits of plotregion from graph
    parse_graph "`graph'" `subgr' // returns Ymin Ymax, Xmin, Xmax
    local Yr = (`Ymax' - `Ymin')
    local Xr = (`Xmax' - `Xmin')
    if `Y'>=.  local Y  = `Ymin' + `y' * `Yr' / 100
    else       local y  = (`Y' - `Ymin') * 100 / `Yr'
    if `X'>=.  local X  = `Xmin' + `x' * `Xr' / 100
    else       local x  = (`X' - `Xmin') * 100 / `Xr'
    if `H'>=.  local H  = `h' * `Yr' / 100
    else       local h  = `H' * 100 / `Yr'
    if `W'>=.  local W  = `w' * `Xr' / 100
    else       local w  = `W' * 100 / `Xr'
    if `TY'>=. local TY = `ty' * `Yr' / 100
    else       local ty = `TY' * 100 / `Yr'
    if `TX'>=. local TX = `tx' * `Xr' / 100
    else       local tx = `TY' * 100 / `Xr'
    if `TW'>=. local TW = `tw' * `Xr' / 100
    else       local tw = `TW' * 100 / `Xr'

    // global settings
    foreach opt in Xmax Xmin Ymax Ymin lskip TW TX TY W H X Y tw tx ty w h x y {
        return scalar `opt' = ``opt''
    }

    // create keys
    local minY `Y'
    local maxY `Y'
    local minX `X'
    local maxX `X'
    local p 0
    forv i=1/`key_n' {
        parse_key `minY' `maxY' `minX' `maxX'/*
            */ `p' `Ymin' `Yr' `Xmin' `Xr' `Y' `X' `H' `W' `"`pstyle'"'/*
            */`"`options'"' `TY' `TX' `TW' `"`place'"' `"`just'"' `"`topts'"'/*
            */ `key_`i'' // returns plots, p, Y, X, H, W, TY, ...
        local legend `legend' `plots'
        local Y = `Y' - `lskip' * `H'
    }

    // create frame
    if "`frame'"!="" {
        if `fr_YM'>=. local fr_YM = `fr_ym' * `Yr' / 100
        else          local fr_xm = `fr_YM' * 100 / `Yr'
        if `fr_XM'>=. local fr_XM = `fr_xm' * `Xr' / 100
        else          local fr_xm = `fr_XM' * 100 / `Xr'
        local fr_B = cond(`Yr'<0,`maxY',`minY') - `fr_YM'
        local fr_T = cond(`Yr'<0,`minY',`maxY') + `fr_YM'
        local fr_L = cond(`Xr'<0,`maxX',`minX') - `fr_XM'
        local fr_R = cond(`Xr'<0,`minX',`maxX') + `fr_XM'
        if `fr_Y'>=. {
            if `fr_y'<. local fr_Y = `Ymin' + `fr_y' * `Yr' / 100
            else            local fr_Y = `fr_T'
        }
        if `fr_y'>=. local fr_y = (`fr_Y' - `Ymin') / `Yr' * 100
        if `fr_H'>=. {
            if `fr_h'<. local fr_H = `fr_y' * `Yr' / 100
            else            local fr_H = `fr_Y' - `fr_B'
        }
        if `fr_h'>=. local fr_h = `fr_H' / `Yr' * 100
        if `fr_X'>=. {
            if `fr_x'<. local fr_X = `Xmin' + `fr_x' * `Xr' / 100
            else            local fr_X = `fr_L'
        }
        if `fr_x'>=. local fr_x = (`fr_X' - `Xmin') / `Xr' * 100
        if `fr_W'>=. {
            if `fr_w'<. local fr_W = `fr_w' * `Xr' / 100
            else            local fr_W = `fr_R' - `fr_X'
        }
        if `fr_w'>=. local fr_w = `fr_W' / `Xr' * 100
        local fr_B = `fr_Y' - `fr_H'
        local fr_R = `fr_X' + `fr_W'
        local legend (scatteri `fr_B' `fr_X' `fr_B' `fr_R' `fr_Y' `fr_R'/*
            */ `fr_Y' `fr_X', recast(area) nodropbase `fr_opts') `legend'
        foreach opt in XM YM W H X Y xm ym w h x y {
            return scalar fr_`opt' = `fr_`opt''
        }
    }

    // return result
    return local legend `"`legend'"'
end

program parse_topts
    syntax [, PLACEment(str) Justification(str) * ]
    c_local place `"`placement'"'
    c_local just `"`justification'"'
    c_local topts `options'
end

program parse_graph
    gettoken name 0 : 0
    gettoken plot 0 : 0
    if `"`name'"'=="" {
        local name `"`._Gr_Global.current_graph'"'
        if `"`name'"'=="" { // no graph available
            di as txt "(no graph found; assuming range 0 to 100 for both axes)"
            c_local Ymin 0
            c_local Ymax 100
            c_local Xmin 0
            c_local Xmax 100
            exit
        }
    }
    // obtain defaults from graph
    capt classutil d `name'
    if _rc {
        di as err `"graph `name' not found"'
        exit 111
    }
    if "`plot'"!="" {
        capt classutil d `name'.graphs[`plot']
        if _rc {
            di as err `"subgraph `plot' not found"'
            exit 111
        }
        local grtype `"`.`name'.graphs[`plot'].graphfamily'"'
        if `"`grtype'"'!="twoway" {
            di as err `"subgraph `plot' not twoway"'
            exit 498
        }
    }
    else {
        local grtype `"`.`name'.graphfamily'"'
        if `"`grtype'"'!="twoway" {
            local plots
            local nplots `"`.`name'.n'"'
            capt confirm integer number `nplots'
            if _rc==0 {
                forv plot = 1/`nplots' {
                    if `"`.`name'.graphs[`plot'].graphfamily'"'=="twoway" {
                        local plots `plots' `plot'
                    }
                }
            }
            if "`plots'"=="" {
                di as err "no twoway subgraphs found in `name'"
                exit 111
            }
            gettoken plot : plots
        }
    }
    if "`plot'"=="" local Name `name'
    else            local Name `name'.graphs[`plot']
    foreach Z in Y X {
        local z = strlower("`Z'")
        local `z'rev `.`Name'.plotregion1.`z'scale.reverse.istrue'
        if `"``z'rev'"'=="1" {
            c_local `Z'min `.`Name'.plotregion1.`z'scale.curmax'
            c_local `Z'max `.`Name'.plotregion1.`z'scale.curmin'
        }
        else {
            c_local `Z'min `.`Name'.plotregion1.`z'scale.curmin'
            c_local `Z'max `.`Name'.plotregion1.`z'scale.curmax'
        }
    }
end

program parse_frame
    syntax [, y(numlist max=1) x(numlist max=1)/*
        */ h(numlist max=1) w(numlist max=1)/*
        */ ym(real 2.5) xm(real 2)/*
        */ LSTYle(passthru) FColor(passthru) * ]
    mata: extract_opts(("Y", "X", "H", "W", "YM", "XM"))
    foreach opt in Y X H W YM XM {
        if "``opt''"!="" numlist `"``opt''"', max(1)
    }
    foreach opt in y x h w Y X H W YM XM {
        if "``opt''"=="" local `opt' .
    }
    foreach opt in y x h w ym xm Y X H W YM XM {
        c_local fr_`opt' `"``opt''"'
    }
    if `"`lstyle'"'=="" local lstyle lstyle(foreground)
    if `"`fcolor'"'=="" local fcolor fcolor(white)
    c_local fr_opts `lstyle' `fcolor' `options'
end

program parse_key
    foreach arg in minY maxY minX maxX p Ymin Yr Xmin Xr _Y _X _H _W/*
        */ _pstyle _options _TY _TX _TW _place _just _topts {
        gettoken `arg' 0 : 0
    }
    _parse comma txt  0 : 0
    syntax [, /*
        */ y(numlist max=1) x(numlist max=1)/*
        */ h(numlist max=1) w(numlist max=1)/*
        */ ty(numlist max=1) tx(numlist max=1) tw(numlist max=1) Text(str)/*
        */ PSTYle(passthru) * ]
    if "`y'"!=""  local _Y  = `Ymin' + `y' * `Yr' / 100
    if "`x'"!=""  local _X  = `Xmin' + `x' * `Xr' / 100
    if "`h'"!=""  local _H  =          `h' * `Yr' / 100
    if "`w'"!=""  local _W  =          `w' * `Xr' / 100
    if "`ty'"!="" local _TY =         `ty' * `Yr' / 100
    if "`tx'"!="" local _TX =         `tx' * `Xr' / 100
    if "`tx'"!="" local _TW =         `tw' * `Xr' / 100
    mata: extract_opts(("Y", "X", "H", "W", "TY", "TX", "TW"))
    foreach opt in Y X H W TY TX TW {
        if "``opt''"!="" numlist `"``opt''"', max(1)
    }
    parse_topts, `text' // returns place, just, topts
    foreach opt in Y X H W TY TX TW pstyle place just {
        if "``opt''"!="" continue
        local `opt' `"`_`opt''"'
    }
    local options `_options' `options'
    local topts `_topts' `topts'

    // collect symbols
    gettoken next : txt
    if `"`next'"'=="-" {
        gettoken next txt : txt
        local sym_n  0
        local hassym 1
    }
    else {
        local i 0
        while (1) {
            gettoken next : txt, match(par)
            if `"`par'"'!="" {
                local ++i
                gettoken sym_`i' txt : txt, match(par)
                continue
            }
            continue, break
        }
        local sym_n `i'
        if `sym_n' {
            local hassym 1
            local p = mod(`++p'-1, 15) + 1
        }
        else local hassym 0
    }

    // plot symbols
    local plots
    forv i=1/`sym_n' {
        parse_sym `minY' `maxY' `minX' `maxX'/*
            */ `p' `Ymin' `Yr' `Xmin' `Xr' `Y' `X' `H' `W'/*
            */ `"`pstyle'"' `"`options'"' `sym_`i'' // return plot
        local plots `plots' `plot'
    }

    // collect label
    parse_txt `txt' // returns txt

    // plot label
    if `TX'>=. local tx = `W' + cond(`W'<0, -1, 1) * abs(`Xr') / 100
    else       local tx `TX'
    local y = `Y'
    local x = `X'
    if `hassym' {
        local y = `y' + `TY'
        local x = `x' + `tx'
    }
    if (sign(`tx')*sign(`Xr'))<0 {
        if `"`place'"'=="" local place left
        if `"`just'"'==""  local just  right
        local tdir -1
    }
    else {
        if `"`place'"'=="" local place right
        if `"`just'"'==""  local just  left
        local tdir 1
    }
    local topts place(`place') just(`just') `topts'
    local plots `plots' (scatteri `y' `x', ms(i) text(`y' `x' `txt', `topts'))
    local minY = min(`minY', `y' - 0.5 * `H', `y' + 0.5 * `H')
    local maxY = max(`maxY', `y' - 0.5 * `H', `y' + 0.5 * `H')
    local minX = min(`minX', `x', `x' + `tdir' * `TW')
    local maxX = max(`maxX', `x', `x' + `tdir' * `TW')

    // returns
    c_local minY `minY'
    c_local maxY `maxY'
    c_local minX `minX'
    c_local maxX `maxX'
    foreach opt in p Y X H W TY TX TW {
        c_local `opt' ``opt''
    }
    c_local plots `plots'
end

program parse_sym
    foreach arg in minY maxY minX maxX p Ymin Yr Xmin Xr _Y _X _H _W/*
        */ _pstyle _options {
        gettoken `arg' 0 : 0
    }
    _parse comma SYM 0 : 0
    if !`: list sizeof SYM' local SYM `""""'
    syntax [, y(numlist max=1) x(numlist max=1)/*
        */ h(numlist max=1) w(numlist max=1)/*
        */ PSTYle(passthru) * ]
    if "`y'"!="" local _Y  = `Ymin' + `y' * `Yr' / 100
    if "`x'"!="" local _X  = `Xmin' + `x' * `Xr' / 100
    if "`h'"!="" local _H  =          `h' * `Yr' / 100
    if "`w'"!="" local _W  =          `w' * `Xr' / 100
    mata: extract_opts(("Y", "X", "H", "W"))
    foreach opt in Y X H W {
        if "``opt''"!="" numlist `"``opt''"', max(1)
    }
    foreach opt in Y X H W pstyle {
        if "``opt''"!="" continue
        local `opt' `"`_`opt''"'
    }
    local options `_options' `options'
    local plots
    foreach sym of local SYM {
        if      `"`sym'"'=="rcap"    local sym "cap"
        else if `"`sym'"'=="rcapsym" local sym "capsym"
        else if `"`sym'"'=="spike"   local sym "line"
        local b = `Y' - 0.5 * `H'
        local t = `Y' + 0.5 * `H'
        local l = `X'
        local r = `X' + `W'
        local minY = min(`minY', `t', `b')
        local maxY = max(`maxY', `t', `b')
        local minX = min(`minX', `l', `r')
        local maxX = max(`maxX', `l', `r')
        local ptype ""
        if inlist(`"`sym'"',"line","cap","capsym") {
            if `"`sym'"'=="line"     local plot recast(line)
            else if `"`sym'"'=="cap" local plot recast(connect) ms(|)
            else                     local plot recast(connect)
            local plot scatteri `Y' `l' `Y' `r', `plot'
        }
        else if inlist(`"`sym'"',"area","bar","rline") {
            if `"`sym'"'=="rline" {
                local plot scatteri `t' `l' `t' `r' . . `b' `l' `b' `r',/*
                    */ recast(line) cmissing(n)
            }
            else {
                local ptype "`sym'"
                local plot scatteri `b' `l' `b' `r' `t' `r' `t' `l',/*
                    */ recast(area) nodropbase
            }
        }
        else {
            local c = `X' + 0.5 * `W'
            local plot ms(`sym')
            if `"`sym'"'=="" local plot
            local plot scatteri `Y' `c', `plot'
        }
        if `"`pstyle'"'=="" local PSTYLE pstyle(p`p'`ptype')
        else                local PSTYLE `pstyle'
        local OPTIONS `PSTYLE' `options'
        local plots `plots' (`plot' `OPTIONS')
    }
    c_local minY `minY'
    c_local maxY `maxY'
    c_local minX `minX'
    c_local maxX `maxX'
    c_local plot `plots'
end

program parse_txt
    c_local txt `"`0'"'
    while (`"`0'"'!="") {
        gettoken tok 0 : 0, qed(qed)
        if `qed' continue
        di as err `"{bf:`tok'} found where quoted text expected"'
        exit 198
    }
end

version 14
mata:
mata set matastrict on

void extract_opts(string rowvector O)
{
    string scalar    opt, arg, options
    transmorphic     t

    t = tokeninit(" ", "", "()")
    tokenset(t, st_local("options"))
    options = ""
    while ((opt = tokenget(t))!="") {
        arg = tokenpeek(t)
        if ((substr(arg,1,1)+substr(arg,-1,1))=="()") { // arg is "(...)"
            (void) tokenget(t)
            if (anyof(O,opt)) {
                st_local(opt, strtrim(substr(arg, 2, strlen(arg)-2)))
                continue
            }
            options = options + " " + opt + arg
            continue
        }
        options = options + " " + opt
    }
    st_local("options", substr(options, 2, .))
}

void keys_expand()
{
    real scalar   i, a, b
    string scalar s, tok
    transmorphic  t

    s = st_global("s(after)")
    t = tokeninit("", "||", (`""""', `"`""'"', "()"))
    tokenset(t, s)
    i = b = 0
    while ((tok = tokenget(t))!="") {
        if (tok=="||") {
            a = b + 1
            b = tokenoffset(t)
            tok = strtrim(substr(s, a, b-a-2))
            if (tok!="") {
                i++
                st_local("key_"+strofreal(i), tok)
            }
            continue
        }
    }
    a = b + 1
    tok = strtrim(substr(s, a, .))
    if (tok!="") {
        i++
        st_local("key_"+strofreal(i), tok)
    }
    st_local("key_n", strofreal(i))
}

end