// Non-positional channels: size, opacity, shape and colour, and the symbol
// legends that describe them.
///|
priv enum LegKind {
Disc(@scale.Ordinal, Array[@value.Value])
Cont(@scale.Continuous)
}
///|
priv struct LegCh {
/// "size" | "shape" | "fill" | "stroke" | "opacity"
channel : String
pos : Pos
kind : LegKind
}
///|
let symbol_shapes : Array[String] = [
"circle", "square", "triangle-up", "cross", "diamond", "triangle-right", "triangle-down",
"triangle-left",
]
///|
/// A linear scale over the extent of `field`, optionally including zero.
fn extent_scale(
rows : Array[Map[String, @value.Value]],
field : String,
zero : Bool,
r0 : Double,
r1 : Double,
) -> @scale.Continuous {
let c = @scale.Continuous::new(Linear)
match domain_extent(rows, [field], zero) {
Some((lo, hi)) => c.domain = [lo, hi]
None => ()
}
c.range = [r0, r1]
c
}
///|
fn canonical_rank(channel : String) -> Int32 {
match channel {
"size" => 0
"shape" => 1
"fill" => 2
"stroke" => 3
_ => 6
}
}
///|
/// Symbol legends: channels encoding the same field share one legend.
fn legends_of(
chs : Array[LegCh],
mark : String,
filled : Bool,
base_color : String,
has_color_field : Bool,
) -> Array[Legend] raise CompileError {
let groups : Array[Array[LegCh]] = []
for c in chs {
let mut placed = false
for g in groups {
if g[0].pos.field == c.pos.field && g[0].pos.title == c.pos.title {
g.push(c)
placed = true
break
}
}
if !placed {
groups.push([c])
}
}
let out : Array[Legend] = []
for g in groups {
let sorted = @transform.stable_sort(g, fn(a, b) {
let d = canonical_rank(a.channel) - canonical_rank(b.channel)
if d < 0 {
-1
} else if d > 0 {
1
} else {
0
}
})
let canon = sorted[0]
let has_color = g.any(fn(c) { c.channel == "fill" || c.channel == "stroke" })
let has_opacity = g.any(fn(c) { c.channel == "opacity" })
let has_shape = g.any(fn(c) { c.channel == "shape" })
let has_size = g.any(fn(c) { c.channel == "size" })
let symbol : Map[String, @value.Value] = Map([("strokeWidth", num(1.5))])
symbol["shape"] = str(
if has_shape {
"circle"
} else {
match mark {
"bar" => "square"
"circle" | "square" => mark
_ => "circle"
}
},
)
if !has_size {
symbol["size"] = num(100.0)
}
if !has_color && has_color_field {
if filled {
symbol["fill"] = str("black")
symbol["fillOpacity"] = num(
if mark == "point" || mark == "circle" || mark == "square" {
0.7
} else {
1.0
},
)
symbol["stroke"] = str("transparent")
} else {
symbol["fill"] = str("transparent")
symbol["stroke"] = str("#888")
}
} else if !has_color {
if filled {
symbol["fill"] = str(base_color)
} else {
symbol["fill"] = str("transparent")
symbol["stroke"] = str(base_color)
}
} else if !g.any(fn(c) { c.channel == "fill" }) {
symbol["fill"] = str("transparent")
}
if !has_opacity &&
(
mark == "point" ||
mark == "circle" ||
mark == "square" ||
mark == "tick"
) {
symbol["opacity"] = num(0.7)
}
let values : Array[@value.Value] = []
let labels : Array[String] = []
match canon.kind {
Disc(_, dom) =>
for v in dom {
values.push(v)
labels.push(v.to_js_string())
}
Cont(c) => {
let d0 = c.domain[0]
let d1 = c.domain[c.domain.length() - 1]
let fmt = @format.format_span(d0, d1, 5.0) catch {
_ => raise CompileError("legend 格式无效")
}
for x in c.ticks(count=5.0) {
values.push(Num(x))
labels.push(fmt(x))
}
}
}
let entries = values.mapi(fn(i, v) {
let props : Map[String, @value.Value] = Map([])
for c in g {
let key = match c.channel {
"fill" | "stroke" | "shape" | "size" | "opacity" => c.channel
k => k
}
match c.kind {
Disc(o, _) =>
match o.map(v) {
Some(x) => props[key] = x
None => ()
}
Cont(sc) =>
match sc.map(v.to_number()) {
Some(x) => props[key] = Num(x)
None => ()
}
}
}
{ label: labels[i], props, }
})
out.push(
build_legend({
title: Some(canon.pos.title),
orient: "right",
symbol,
entries,
}),
)
}
out
}