Skip to content

Commit f2092b5

Browse files
committed
update docs for scale and legend
1 parent 3601106 commit f2092b5

File tree

5 files changed

+312
-154
lines changed

5 files changed

+312
-154
lines changed

src/core/guide/CoreGuide.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const guide = {
1414
gradientbar: CoreGuideGradientbar,
1515
}
1616
const title = computed(() => {
17-
return scales.map(([s]) => s.title).find(v => v != null)
17+
return scales.reduce((v, [s]) => v ?? s.title, null) ?? scales.reduce((v, [s]) => v ?? s._title, null)
1818
})
1919
const breaks = computed(() => {
2020
let scale_funcs = scales.map(([s]) => s)

src/js/plot.js

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ function object_map(obj, expr) {
99
if (obj == null) return {}
1010
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, expr(k, v)]))
1111
}
12+
function is_categorical(v) {
13+
return typeof v === 'string' ||
14+
typeof v === 'boolean' ||
15+
typeof v === 'symbol' ||
16+
typeof v === 'object' && typeof v.valueOf() !== 'number' && typeof v.valueOf() !== 'bigint'
17+
}
1218

1319
/**
1420
* Graphic Layer object
@@ -68,9 +74,9 @@ class GLayer {
6874
if ($$data == null) $$data = $data
6975
$$aes = { ...$aes, ...$$aes }
7076
let data = {}
71-
let fns = {} // function names for legend titles
77+
this.$fnames = {} // function names for legend titles
7278
for (const aes in $$aes) {
73-
fns[aes] = String($$aes[aes])
79+
this.$fnames[aes] = String($$aes[aes])
7480
data[aes] = $$data.map($$aes[aes])
7581
}
7682
data.$raw = $$data
@@ -119,21 +125,18 @@ class GLayer {
119125
])
120126

121127
this.$data = data
122-
this.$fns = fns
123128
this.data = { ...data }
124129
for (const aes in data) {
125130
if ($$scales[aes] != null) {
126131
let scale = new Scale($$scales[aes])
127132
scale.aesthetics = aes
128-
if (scale.title === undefined) {
129-
scale.title = fns[aes]
130-
}
133+
scale._title = this.$fnames[aes]
131134
let values = this.$data[aes]
132135
if (!values?.length) continue
133136
if (!scale.asis) {
134137
if ($$levels?.[aes] != null) {
135138
scale.level = $$levels[aes]
136-
} else if (values.some(v => typeof v === 'string')) {
139+
} else if (values.some(is_categorical)) {
137140
scale.level = GEnumLevel.from(values)
138141
} else {
139142
scale.extent = numutils.extent(values)
@@ -208,15 +211,13 @@ export class GPlot {
208211
if (!scale.asis) {
209212
if (levels?.[aes] != null) {
210213
scale.level = levels[aes]
211-
} else if (values.some(v => typeof v === 'string')) {
214+
} else if (values.some(is_categorical)) {
212215
scale.level = GEnumLevel.from(values)
213216
} else {
214217
scale.extent = numutils.extent(values)
215218
}
216219
}
217-
if (scale.title == null) {
218-
scale.title = this.layers.map(layer => layer.$fns?.[aes]).find(s => s != null)
219-
}
220+
scale._title = this.layers.reduce((v, layer) => v ?? layer.$fnames?.[aes], null)
220221
for (const layer of this.layers) {
221222
if (!layer.localScales.has(aes)) {
222223
layer.applyScale(aes, scale)
@@ -252,7 +253,7 @@ export class GPlot {
252253
let fn = vvgeom[layer.geom].get_values ?? vvgeom[layer.geom].get_range
253254
return fn?.(layer.$data, aes)
254255
})
255-
if (values.some(v => typeof v === 'string')) {
256+
if (values.some(is_categorical)) {
256257
this.levels[aes] = GEnumLevel.from(values)
257258
} else {
258259
values = this.layers.flatMap(layer => vvgeom[layer.geom].get_range?.(layer.$data, aes)).filter(v => !isNaN(v))
@@ -373,6 +374,9 @@ export class Scale extends Function {
373374
max = this.$limits?.max ?? this.level?.length ?? this._limits?.max
374375
return { 0: min, 1: max, length: 2, min, max }
375376
}
377+
toString() {
378+
return this.title ?? String(this._fn)
379+
}
376380
}
377381

378382
class DiscreteCoordScale extends Function {

web-docs/docs/legend.vue

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<script setup>
2+
import { ref } from 'vue'
3+
import vvscale from '#base/js/scale'
4+
5+
import iris from '../data/iris.json'
6+
import CO2 from '../data/CO2.json'
7+
const vBind = { CO2, iris, vvscale }
8+
const templates = ref({})
9+
</script>
10+
<template>
11+
<article>
12+
<section>
13+
<h2>Plot legend</h2>
14+
<p>
15+
In VVPlot, legends will be rendered in the DOM element specified by the
16+
<code>legend-teleport</code> prop of the <code>&lt;VVPlot&gt;</code> component.
17+
</p>
18+
<hr>
19+
<pre-highlight lang="html">{{templates[1] = `<VVPlot :data="CO2" :width="600" :height="400" legend-teleport="#legend-1">
20+
<VVGeomPoint :x="d => d.conc" :y="d => d.uptake" :color="d => d.Type" :shape="d => d.Treatment" />
21+
<VVGeomLine :x="d => d.conc" :y="d => d.uptake" :color="d => d.Type" :group="d => d.Plant" />
22+
</VVPlot>
23+
<div id="legend-1" class="p-1 border"></div>` }}</pre-highlight>
24+
<div class="flex flex-row items-start">
25+
<component :is="{ template: templates[1], props: Object.keys(vBind) }" v-bind="vBind" />
26+
</div>
27+
<hr>
28+
<p>
29+
Discrete aesthetics mapping to the same categorical levels will be merged into a single guide entry.
30+
</p>
31+
<hr>
32+
<pre-highlight lang="html">{{templates[2] = `<VVPlot :data="iris" :width="600" :height="400" legend-teleport="#legend-2">
33+
<VVGeomEllipse :x="d => d.Petal_Width" :y="d => d.Sepal_Length"
34+
:color="d => d.Species" :fill="d => d.Species"
35+
:scales="{ fill: vvscale.fill.default({ alpha: 0.2 }) }" />
36+
<VVGeomPoint :x="d => d.Petal_Width" :y="d => d.Sepal_Length"
37+
:color="d => d.Species" :shape="d => d.Species" />
38+
</VVPlot>
39+
<div id="legend-2"></div>` }}</pre-highlight>
40+
<div class="flex flex-row">
41+
<component :is="{ template: templates[2], props: Object.keys(vBind) }" v-bind="vBind" />
42+
</div>
43+
<hr>
44+
<p>
45+
To split legends for different aesthetics with the same categorical levels,
46+
you can set different <code>key</code> properties for the scaling functions.
47+
</p>
48+
<p>
49+
The first available <code>title</code> property of the scaling function will be used as the title label
50+
of the legend guide.
51+
If none is set, the string representation of the aesthetic function will be used.
52+
</p>
53+
<hr>
54+
<pre-highlight lang="html">{{templates[3] = `<VVPlot :data="iris" :width="600" :height="400" :scales="{
55+
color: vvscale.color.default({ key: 'color', title: 'Color and Fill' }),
56+
fill: vvscale.fill.default({ key: 'color', alpha: 0.2 }),
57+
shape: vvscale.shape.default({ key: 'shape' })
58+
}" legend-teleport="#legend-3">
59+
<VVGeomEllipse :x="d => d.Petal_Width" :y="d => d.Sepal_Length"
60+
:color="d => d.Species" :fill="d => d.Species" />
61+
<VVGeomPoint :x="d => d.Petal_Width" :y="d => d.Sepal_Length" :color="d => d.Species"
62+
:shape="Object.assign(d => d.Species, { toString: () => 'Shape' })" />
63+
</VVPlot>
64+
<div id="legend-3"></div>` }}</pre-highlight>
65+
<div class="flex flex-row">
66+
<component :is="{ template: templates[3], props: Object.keys(vBind) }" v-bind="vBind" />
67+
</div>
68+
<hr>
69+
<p>
70+
You can put the legend element in the <code>#panel</code> slot of the <code>&lt;VVPlot&gt;</code>
71+
component to place it relatively to the plot area.
72+
</p>
73+
<blockquote class="info">
74+
Elements within the <code>#panel</code> slot will be slotted into an absolutely positioned div that
75+
covers the entire plot area.
76+
</blockquote>
77+
<hr>
78+
<pre-highlight lang="html">{{templates[4] = `<VVPlot :data="iris" :width="600" :height="400" legend-teleport="#legend-4">
79+
<VVGeomPoint :x="d => d.Petal_Width" :y="d => d.Sepal_Length"
80+
:color="d => d.Species" :shape="d => d.Species" />
81+
<template #panel>
82+
<div id="legend-4" class="absolute bottom-0 right-0"></div>
83+
</template>
84+
</VVPlot>` }}</pre-highlight>
85+
<component :is="{ template: templates[4], props: Object.keys(vBind) }" v-bind="vBind" />
86+
</section>
87+
</article>
88+
89+
</template>

0 commit comments

Comments
 (0)