// Layered views (`layer`): the layers are flattened into unit specifications
// that inherit the encoding, data and transforms of their parents, the scales
// of the position channels are shared between them, and their marks are drawn
// one on top of the other inside one plot with one set of axes.
///|
priv struct Leaf {
/// JSON path of the layer ("layer[1]", "layer[0].layer[2]")
path : String
/// the name vega-lite gives its marks ("layer_1", "layer_0_layer_2")
name : String
/// the unit specification with everything inherited filled in
spec : @spec.Spec
/// whether its data is the top-level data with only top-level transforms
/// before the layer's own ones (so that a bin can be shared by all layers)
on_root_data : Bool
}
///|
fn[T] or_else(inner : T?, outer : T?) -> T? {
match inner {
Some(_) => inner
None => outer
}
}
///|
/// A channel definition of a layer on top of the one it inherits: every property
/// the layer sets wins, the others are kept (so `y: {field: "b"}` inside a layer
/// of `y: {aggregate: "mean", field: "a"}` means the mean of `b`). A constant
/// `value` replaces the inherited definition altogether.
fn merge_def(
outer : @spec.ChannelDef,
inner : @spec.ChannelDef,
) -> @spec.ChannelDef {
if inner.value is Some(_) || inner.datum is Some(_) {
return inner
}
{
field: or_else(inner.field, outer.field),
typ: or_else(inner.typ, outer.typ),
aggregate: or_else(inner.aggregate, outer.aggregate),
bin: or_else(inner.bin, outer.bin),
binned: inner.binned || outer.binned,
time_unit: or_else(inner.time_unit, outer.time_unit),
sort: or_else(inner.sort, outer.sort),
scale: or_else(inner.scale, outer.scale),
scale_off: inner.scale_off || outer.scale_off,
axis: match inner.axis {
Default => outer.axis
other => other
},
legend: match inner.legend {
Default => outer.legend
other => other
},
title: or_else(inner.title, outer.title),
format: or_else(inner.format, outer.format),
format_type: or_else(inner.format_type, outer.format_type),
value: outer.value,
datum: outer.datum,
stack: or_else(inner.stack, outer.stack),
band: or_else(inner.band, outer.band),
band_position: or_else(inner.band_position, outer.band_position),
}
}
///|
/// Merge `encoding` definitions channel by channel (see `merge_def`).
fn merge_encoding(
outer : Array[(String, @spec.ChannelDef)],
inner : Array[(String, @spec.ChannelDef)],
) -> Array[(String, @spec.ChannelDef)] {
let out = outer.copy()
for e in inner {
let mut replaced = false
for i in 0.. Array[Leaf] raise CompileError {
let out : Array[Leaf] = []
fn walk(
node : @spec.Spec,
path : String,
name : String,
chain : Array[@spec.Spec],
) -> Unit raise CompileError {
if path != "" {
if node.width is Some(_) {
raise nope(@spec.join_path(path, "width"))
}
if node.height is Some(_) {
raise nope(@spec.join_path(path, "height"))
}
if !node.resolve.scale.is_empty() ||
!node.resolve.axis.is_empty() ||
!node.resolve.legend.is_empty() {
raise nope(@spec.join_path(path, "resolve"))
}
}
if node.layer.length() > 0 {
for i in 0..= owner {
for t in chain[i].transform {
transform.push(t)
}
if i < chain.length() - 1 {
ancestor_transforms += chain[i].transform.length()
}
}
encoding = merge_encoding(encoding, chain[i].encoding)
}
out.push({
path,
name,
on_root_data: owner == 0 && ancestor_transforms == top.transform.length(),
spec: {
layer: [],
resolve: top.resolve,
mark: node.mark,
encoding,
transform,
data: chain[owner].data,
width: top.width,
height: top.height,
padding: top.padding,
background: top.background,
title: top.title,
description: None,
config: Map([]),
},
})
}
walk(top, "", "", [top])
out
}
///|
/// Whether a position scale is band-like (bar, rect, tick, rule on a discrete
/// field) - such a scale wins when band and point scales are merged.
fn is_band_mark(mark : String) -> Bool {
mark == "bar" || mark == "rect" || mark == "tick" || mark == "rule"
}
///|
/// Titles of the layers of a shared guide, merged pairwise as vega-lite's
/// `mergeTitleComponent` does: an explicit title (set on the channel) beats an
/// implicit one, otherwise a title is appended unless it repeats the previous
/// result or is empty (a datum has no title).
fn merge_titles_all(ps : Array[Pos]) -> String {
let mut title = ""
let mut explicit = false
for i in 0...`, else shared.
fn resolve_mode(top : @spec.Spec, kind : String, channel : String) -> String {
let m = match kind {
"scale" => top.resolve.scale
"axis" => top.resolve.axis
_ => top.resolve.legend
}
match m.get(channel) {
Some(v) => v
None => "shared"
}
}
///|
/// The kind of a position scale for merging: vega-lite merges scales of the
/// same category only (numeric / time / discrete position).
fn scale_category(p : Pos) -> String {
if p.discrete {
"discrete"
} else if p.temporal {
"time"
} else {
match p.def.scale {
Some(m) =>
match m.get("type") {
Some(Str(t)) => "numeric:" + t
_ => "numeric:linear"
}
None => "numeric:linear"
}
}
}
///|
/// Layers that bin the same field in the same way share one bin transform:
/// vega-lite's optimizer hoists it above the layers' own transforms, so the
/// bins come from the extent of the data they have in common.
fn shared_layer_bins(
top : @spec.Spec,
leaves : Array[Leaf],
) -> Map[String, @transform.BinParams] raise CompileError {
let out : Map[String, @transform.BinParams] = Map([])
for chan in ["x", "y"] {
let binned : Array[(Leaf, String, Map[String, @value.Value])] = []
for leaf in leaves {
match find_channel(leaf.spec, chan) {
Some({ field: Some(f), bin: Some(m), aggregate: None, .. }) =>
binned.push((leaf, f, m))
_ => ()
}
}
if binned.length() < 2 {
continue
}
let signature = fn(f : String, m : Map[String, @value.Value]) -> String {
let keys : Array[String] = []
for k, _ in m {
keys.push(k)
}
keys.sort()
f + "|" + keys.map(fn(k) { k + "=" + m[k].to_js_string() }).join(";")
}
let first = signature(binned[0].1, binned[0].2)
for b in binned {
if signature(b.1, b.2) != first || !b.0.on_root_data {
raise nope(
b.0.path +
".encoding." +
chan +
".bin (多个 layer 的分箱不一致)",
)
}
}
let data = match top.data {
Some(d) => d
None => raise CompileError("data 缺失")
}
let rows = apply_transforms(data.map(fn(r) { r.copy() }), top.transform)
out[chan] = bin_params(rows, binned[0].1, binned[0].2, 10.0)
}
out
}
///|
/// Compile a layered specification.
fn compile_layers(top : @spec.Spec) -> Compiled raise CompileError {
validate_config(top.config)
let leaves = flatten_layers(top)
let shared_bins = shared_layer_bins(top, leaves)
let units : Array[UnitModel] = []
for leaf in leaves {
try validate(leaf.spec) catch {
CompileError(m) => raise CompileError(leaf.path + "." + m)
} noraise {
_ => ()
}
units.push(build_unit(leaf.spec, shared_bins~))
}
let n = units.length()
let pos_of_unit = fn(i : Int32, chan : String) -> Pos {
(if chan == "x" { units[i].px } else { units[i].py }).unwrap()
}
// --- position scales: shared by the layers that encode the channel, unless
// the specification says otherwise or the scales cannot be merged
let members_of = fn(chan : String) -> Array[Int32] {
let members : Array[Int32] = []
for i in 0.. Bool {
if members.length() < 2 {
return true
}
let mode = resolve_mode(top, "scale", chan)
let first = scale_category(pos_of_unit(members[0], chan))
let compatible = members.all(fn(i) {
scale_category(pos_of_unit(i, chan)) == first
})
mode != "independent" && compatible
}
let x_shared = shared_pos("x", xm)
let y_shared = shared_pos("y", ym)
// separate axes on a shared scale are not implemented
for chan in ["x", "y"] {
let shared = if chan == "x" { x_shared } else { y_shared }
let members = if chan == "x" { xm } else { ym }
if shared &&
members.length() > 1 &&
resolve_mode(top, "axis", chan) == "independent" {
raise nope("resolve.axis." + chan + " independent (共用比例尺)")
}
}
for chan in ["x", "y"] {
let members = if chan == "x" { xm } else { ym }
let shared = if chan == "x" { x_shared } else { y_shared }
if !shared && members.any(fn(i) { pos_of_unit(i, chan).discrete }) {
raise nope("resolve.scale." + chan + " independent (离散比例尺)")
}
}
// the unit that gives a merged scale its style: a band scale wins
let style_unit = fn(members : Array[Int32]) -> Int32 {
for i in members {
if is_band_mark(units[i].mark) {
return i
}
}
members[0]
}
let srcs_of = fn(members : Array[Int32], chan : String) {
members.map(fn(i) {
(
units[i].data,
if chan == "x" {
units[i].xfields
} else {
units[i].yfields
},
)
})
}
let order_of = fn(
members : Array[Int32],
chan : String,
) -> Array[@value.Value]? {
for i in members {
let o = if chan == "x" { units[i].x_order } else { units[i].y_order }
if o is Some(_) {
return o
}
}
None
}
let size_of = fn(
members : Array[Int32],
chan : String,
shared : Bool,
) -> Double {
let size = if chan == "x" { top.width } else { top.height }
if members.length() == 0 {
return axis_size(None, 0, "", step_of(size), 200.0, size)
}
let discrete = pos_of_unit(members[0], chan).discrete
let si = style_unit(members)
let count = if discrete && shared {
sorted_unique_multi(srcs_of(members, chan)).length()
} else {
0
}
axis_size(
Some(pos_of_unit(members[0], chan)),
count,
units[si].mark,
step_of(size),
200.0,
size,
)
}
let width = size_of(xm, "x", x_shared)
let height = size_of(ym, "y", y_shared)
// a binned layer leads a shared scale: its bins give the ticks and the style
let is_binned = fn(i : Int32, chan : String) -> Bool {
pos_of_unit(i, chan).def.bin is Some(_) && units[i].bin_info.contains(chan)
}
let lead_of = fn(members : Array[Int32], chan : String) -> Int32 {
for i in members {
if is_binned(i, chan) {
return i
}
}
members[0]
}
let make_scale = fn(
members : Array[Int32],
chan : String,
r0 : Double,
r1 : Double,
) -> (RScale, Array[@value.Value]) {
let si = style_unit(members)
let f = lead_of(members, chan)
let u = units[f]
let binned = members.filter(fn(i) { is_binned(i, chan) })
let others = members.filter(fn(i) { !is_binned(i, chan) })
let result = pos_scale(
pos_of_unit(f, chan),
srcs_of(if binned.length() > 0 { binned } else { members }, chan),
units[si].mark,
r0,
r1,
u.bin_info.get(chan),
zero_of_chan(
u.mark,
u.orient,
chan,
if chan == "x" {
u.px2 is Some(_)
} else {
u.py2 is Some(_)
},
u.config_zero,
),
order_of(members, chan),
)
// other layers sharing a binned scale widen its domain, which is then niced
match result.0 {
Binned(c, _) if others.length() > 0 =>
match domain_extent_multi(srcs_of(others, chan), false) {
Some((lo, hi)) => {
c.domain = [min(c.domain[0], lo), max(c.domain[1], hi)]
c.nice()
}
None => ()
}
_ => ()
}
result
}
// one scale per layer (layers of a shared channel point at the same one)
let xs_per : Array[(RScale, Array[@value.Value])?] = Array::make(n, None)
let ys_per : Array[(RScale, Array[@value.Value])?] = Array::make(n, None)
for chan in ["x", "y"] {
let members = if chan == "x" { xm } else { ym }
let shared = if chan == "x" { x_shared } else { y_shared }
let target = if chan == "x" { xs_per } else { ys_per }
let range = fn(i : Int32) -> (Double, Double) {
if chan == "x" {
(0.0, width)
} else if pos_of_unit(i, chan).discrete {
(0.0, height)
} else {
(height, 0.0)
}
}
if members.length() > 0 {
if shared {
let (r0, r1) = range(members[0])
let sc = make_scale(members, chan, r0, r1)
for i in members {
target[i] = Some(sc)
}
} else {
for i in members {
let (r0, r1) = range(i)
target[i] = Some(make_scale([i], chan, r0, r1))
}
}
}
}
// --- non-positional scales: colours are shared by default, the other
// channels are only supported when one layer uses them (or when independent)
let color_users : Array[Int32] = []
for i in 0.. u.psize is Some(_)
"opacity" => u.popacity is Some(_)
_ => u.pshape is Some(_)
}
})
// a size scale may be shared when each layer keeps its own legend
let separate_legends = ch == "size" &&
resolve_mode(top, "legend", ch) == "independent"
if users.length() > 1 &&
resolve_mode(top, "scale", ch) != "independent" &&
!separate_legends {
raise nope("encoding." + ch + " (多个 layer 使用同一通道)")
}
}
// one size scale over the domains of all layers that encode size
let size_users : Array[Int32] = []
for i in 0.. 1 &&
resolve_mode(top, "scale", "size") != "independent"
let merged_size : @scale.Continuous? = if size_shared {
let s = units[size_users[0]]
let base = unit_scales(s, width, height).size_scale.unwrap()
match
domain_extent_multi(
size_users.map(fn(i) {
(units[i].data, [units[i].psize.unwrap().field])
}),
true,
) {
Some((lo, hi)) => base.domain = [lo, hi]
None => ()
}
Some(base)
} else {
None
}
let color_shared = color_users.length() > 1 &&
resolve_mode(top, "scale", "color") != "independent"
if color_shared {
for i in color_users {
let c = units[i].pcolor.unwrap()
if c.typ == "quantitative" {
raise nope("encoding.color (多个 layer 共用连续颜色)")
}
}
if resolve_mode(top, "legend", "color") == "independent" {
raise nope("resolve.legend.color independent (共用颜色比例尺)")
}
}
let merged_color = @scale.Ordinal::new()
let merged_color_domain = if color_shared {
sorted_unique_multi(
color_users.map(fn(i) {
(units[i].data, [units[i].pcolor.unwrap().field])
}),
)
} else {
[]
}
set_color_range(
merged_color,
merged_color_domain,
color_users.any(fn(i) { units[i].pcolor.unwrap().typ == "ordinal" }),
)
// --- marks and legends, one mark group per layer in layer order
let marks : Array[@scene.Mark] = []
let legends : Array[Legend] = []
let scales : Array[UnitScales] = []
for i in 0.. { ..sc, size_scale: Some(ms), }
_ => sc
}
scales.push(sc)
marks.push(unit_mark(u, sc, width, height, name=leaves[i].name + "_marks"))
let first_color = color_shared && color_users[0] == i
// the legend of a shared colour scale carries the titles of all its layers
let u_legend = if first_color {
let c = u.pcolor.unwrap()
let joined = merge_titles_all(
color_users.map(fn(k) { units[k].pcolor.unwrap() }),
)
{ ..u, pcolor: Some({ ..c, title: joined, }), }
} else {
u
}
// layers sharing a colour scale give one legend: "circle" is preferred over
// other symbol shapes and the first opacity found is kept
let shape_hint : String? = if first_color {
let mut shape = legend_symbol_shape(units[color_users[0]].mark)
for k in color_users {
let st = legend_symbol_shape(units[k].mark)
if st == "circle" {
shape = st
}
}
Some(shape)
} else {
None
}
let opacity_hint : Double? = if first_color {
let mut found : Double? = None
for k in color_users {
if found is None {
found = mark_opacity(units[k].mark, units[k].spec)
}
}
found
} else {
None
}
// a layer colouring by stroke and another by fill give a legend with both
let channels : Array[String]? = if first_color {
let chs : Array[String] = []
for k in color_users {
let ch = if is_filled_mark(units[k].mark, units[k].spec) {
"fill"
} else {
"stroke"
}
if !chs.contains(ch) {
chs.push(ch)
}
}
Some(chs)
} else {
None
}
let made = unit_legends(
u_legend,
sc,
height,
skip_color=color_shared && !first_color,
shape_override?=shape_hint,
opacity_override?=opacity_hint,
color_channels?=channels,
)
for l in made {
legends.push(l)
}
}
// --- axes: one per shared channel (titles joined), one per layer when the
// scales are independent; the second axis of a channel goes to the other side
let axes_grid : Array[Axis] = []
let axes_main : Array[Axis] = []
let count_orient : Map[String, Int32] = Map([])
let pick_orient = fn(default : String, opposite : String) -> String {
let c = count_orient.get(default).unwrap_or(0)
let co = count_orient.get(opposite).unwrap_or(0)
if c > 0 && c > co {
opposite
} else {
default
}
}
let first_x = if xm.length() > 0 { Some(xm[0]) } else { None }
let first_y = if ym.length() > 0 { Some(ym[0]) } else { None }
for chan in ["x", "y"] {
let members = if chan == "x" { xm } else { ym }
let shared = if chan == "x" { x_shared } else { y_shared }
let owners = if shared && members.length() > 0 {
[lead_of(members, chan)]
} else {
members
}
for k in 0.. units[f].px
None => None
}
let other_y = match first_y {
Some(f) => units[f].py
None => None
}
let au : UnitModel = if chan == "x" {
{ ..u, px: Some(titled), py: other_y, }
} else {
{ ..u, px: other_x, py: Some(titled), }
}
let sc = {
..scales[i],
xs: match first_x {
Some(f) => if chan == "x" { xs_per[i] } else { xs_per[f] }
None => None
},
ys: match first_y {
Some(f) => if chan == "y" { ys_per[i] } else { ys_per[f] }
None => None
},
}
let produced = unit_axes(
au,
sc,
width,
height,
x_orient=if chan == "x" { orient } else { "bottom" },
y_orient=if chan == "y" { orient } else { "left" },
with_x=chan == "x",
with_y=chan == "y",
with_grid=k == 0,
)
for a in produced {
if a.has_grid && !a.has_labels {
axes_grid.push(a)
} else {
axes_main.push(a)
}
}
}
}
let axes : Array[Axis] = []
for a in axes_grid {
axes.push(a)
}
for a in axes_main {
axes.push(a)
}
let title = match top.title {
Some(t) => Some(build_title(t))
None => None
}
assemble(
top,
axes,
marks,
legends,
title,
width,
height,
framed=units.any(fn(u) { u.px is Some(_) || u.py is Some(_) }),
)
}