@genome-spy/core 0.28.3 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/schema.json CHANGED
@@ -3325,7 +3325,8 @@
3325
3325
  "type": "object"
3326
3326
  },
3327
3327
  "samples": {
3328
- "$ref": "#/definitions/SampleDef"
3328
+ "$ref": "#/definitions/SampleDef",
3329
+ "description": "Sample metadata definition. If the object is empty, the sample identifiers will be inferred from the data."
3329
3330
  },
3330
3331
  "spec": {
3331
3332
  "anyOf": [
@@ -3335,7 +3336,8 @@
3335
3336
  {
3336
3337
  "$ref": "#/definitions/UnitSpec"
3337
3338
  }
3338
- ]
3339
+ ],
3340
+ "description": "The view specification to be repeated for each sample."
3339
3341
  },
3340
3342
  "stickySummaries": {
3341
3343
  "type": "boolean"
@@ -3871,19 +3873,23 @@
3871
3873
  "additionalProperties": false,
3872
3874
  "properties": {
3873
3875
  "barScale": {
3874
- "$ref": "#/definitions/Scale"
3876
+ "$ref": "#/definitions/Scale",
3877
+ "description": "An optional scale definition for mapping the attribute to the width of a metadata rectangle."
3875
3878
  },
3876
3879
  "scale": {
3877
3880
  "$ref": "#/definitions/Scale",
3878
- "description": "Color scale (primary)"
3881
+ "description": "Scale definition for the (default) color channel"
3879
3882
  },
3880
3883
  "type": {
3881
- "$ref": "#/definitions/Type"
3884
+ "$ref": "#/definitions/Type",
3885
+ "description": "The attribute type. One of `\"nominal\"`, `\"ordinal\"`, or `\"quantitative\"`."
3882
3886
  },
3883
3887
  "visible": {
3888
+ "description": "Whether the attribute is visible by default.",
3884
3889
  "type": "boolean"
3885
3890
  },
3886
3891
  "width": {
3892
+ "description": "Width of the column in pixels.",
3887
3893
  "type": "number"
3888
3894
  }
3889
3895
  },
@@ -3895,14 +3901,72 @@
3895
3901
  "SampleDef": {
3896
3902
  "additionalProperties": false,
3897
3903
  "properties": {
3904
+ "attributeLabelAngle": {
3905
+ "description": "Angle to be added to the default label angle (-90).\n\n**Default value:** `0`",
3906
+ "type": "number"
3907
+ },
3908
+ "attributeLabelFont": {
3909
+ "description": "The font typeface. GenomeSpy uses [SDF](https://github.com/Chlumsky/msdfgen) versions of [Google Fonts](https://fonts.google.com/). Check their availability at the [A-Frame Fonts](https://github.com/etiennepinchon/aframe-fonts/tree/master/fonts) repository. System fonts are **not** supported.\n\n**Default value:** `\"Lato\"`",
3910
+ "type": "string"
3911
+ },
3912
+ "attributeLabelFontSize": {
3913
+ "description": "The font size in pixels.\n\n**Default value:** `11`",
3914
+ "type": "number"
3915
+ },
3916
+ "attributeLabelFontStyle": {
3917
+ "$ref": "#/definitions/FontStyle",
3918
+ "description": "The font style. Valid values: `\"normal\"` and `\"italic\"`.\n\n**Default value:** `\"normal\"`"
3919
+ },
3920
+ "attributeLabelFontWeight": {
3921
+ "$ref": "#/definitions/FontWeight",
3922
+ "description": "The font weight. The following strings and numbers are valid values: `\"thin\"` (`100`), `\"light\"` (`300`), `\"regular\"` (`400`), `\"normal\"` (`400`), `\"medium\"` (`500`), `\"bold\"` (`700`), `\"black\"` (`900`)\n\n**Default value:** `\"regular\"`"
3923
+ },
3924
+ "attributeSize": {
3925
+ "description": "Default size (width) of the metadata attribute columns. Can be configured per attribute using the `attributes` property.\n\n**Default value:** `10`",
3926
+ "type": "number"
3927
+ },
3928
+ "attributeSpacing": {
3929
+ "description": "Spacing between attribute columns in pixels.\n\n**Default value:** `1`",
3930
+ "type": "number"
3931
+ },
3898
3932
  "attributes": {
3899
3933
  "additionalProperties": {
3900
3934
  "$ref": "#/definitions/SampleAttributeDef"
3901
3935
  },
3936
+ "description": "Explicitly specify the sample attributes.",
3902
3937
  "type": "object"
3903
3938
  },
3904
3939
  "data": {
3905
- "$ref": "#/definitions/Data"
3940
+ "$ref": "#/definitions/Data",
3941
+ "description": "Optional metadata about the samples."
3942
+ },
3943
+ "labelAlign": {
3944
+ "$ref": "#/definitions/Align",
3945
+ "description": "The horizontal alignment of the text. One of `\"left\"`, `\"center\"`, or `\"right\"`.\n\n**Default value:** `\"left\"`"
3946
+ },
3947
+ "labelFont": {
3948
+ "description": "The font typeface. GenomeSpy uses [SDF](https://github.com/Chlumsky/msdfgen) versions of [Google Fonts](https://fonts.google.com/). Check their availability at the [A-Frame Fonts](https://github.com/etiennepinchon/aframe-fonts/tree/master/fonts) repository. System fonts are **not** supported.\n\n**Default value:** `\"Lato\"`",
3949
+ "type": "string"
3950
+ },
3951
+ "labelFontSize": {
3952
+ "description": "The font size in pixels.\n\n**Default value:** `11`",
3953
+ "type": "number"
3954
+ },
3955
+ "labelFontStyle": {
3956
+ "$ref": "#/definitions/FontStyle",
3957
+ "description": "The font style. Valid values: `\"normal\"` and `\"italic\"`.\n\n**Default value:** `\"normal\"`"
3958
+ },
3959
+ "labelFontWeight": {
3960
+ "$ref": "#/definitions/FontWeight",
3961
+ "description": "The font weight. The following strings and numbers are valid values: `\"thin\"` (`100`), `\"light\"` (`300`), `\"regular\"` (`400`), `\"normal\"` (`400`), `\"medium\"` (`500`), `\"bold\"` (`700`), `\"black\"` (`900`)\n\n**Default value:** `\"regular\"`"
3962
+ },
3963
+ "labelLength": {
3964
+ "description": "How much space in pixels to reserve for the labels.\n\n**Default:** `140`",
3965
+ "type": "number"
3966
+ },
3967
+ "labelTitleText": {
3968
+ "description": "Text in the label title\n\n**Default:** `\"Sample name\"`",
3969
+ "type": "string"
3906
3970
  }
3907
3971
  },
3908
3972
  "type": "object"
@@ -3927,6 +3991,7 @@
3927
3991
  },
3928
3992
  "SampleSpec": {
3929
3993
  "additionalProperties": false,
3994
+ "description": "A view specification for a SampleView. This is only functional in the GenomeSpy app.",
3930
3995
  "properties": {
3931
3996
  "baseUrl": {
3932
3997
  "type": "string"
@@ -4012,7 +4077,8 @@
4012
4077
  "type": "object"
4013
4078
  },
4014
4079
  "samples": {
4015
- "$ref": "#/definitions/SampleDef"
4080
+ "$ref": "#/definitions/SampleDef",
4081
+ "description": "Sample metadata definition. If the object is empty, the sample identifiers will be inferred from the data."
4016
4082
  },
4017
4083
  "spec": {
4018
4084
  "anyOf": [
@@ -4022,7 +4088,8 @@
4022
4088
  {
4023
4089
  "$ref": "#/definitions/UnitSpec"
4024
4090
  }
4025
- ]
4091
+ ],
4092
+ "description": "The view specification to be repeated for each sample."
4026
4093
  },
4027
4094
  "stickySummaries": {
4028
4095
  "type": "boolean"
@@ -4478,10 +4545,10 @@
4478
4545
  },
4479
4546
  "baseField": {
4480
4547
  "$ref": "#/definitions/Field",
4481
- "description": "The field that contains the base or amino acid. Used for information content calculation when the offset is `\"information\"`. The data items that have `null` in the baseField are considered gaps and they are taken into account when scaling the the locus' information content."
4548
+ "description": "The field that contains the base or amino acid. Used for information content calculation when the offset is `\"information\"`. The data objects that have `null` in the baseField are considered gaps and they are taken into account when scaling the the locus' information content."
4482
4549
  },
4483
4550
  "cardinality": {
4484
- "description": "Cardinality, e.g., the number if distinct bases or amino acids. Used for information content calculation when the offset is `\"information\"`.\n\n**Default:** `4`;",
4551
+ "description": "Cardinality, e.g., the number if distinct bases or amino acids. Used for information content calculation when the offset is `\"information\"`.\n\n**Default:** `4`",
4485
4552
  "type": "number"
4486
4553
  },
4487
4554
  "field": {
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "contributors": [],
9
9
  "license": "MIT",
10
- "version": "0.28.3",
10
+ "version": "0.30.0",
11
11
  "main": "dist/index.js",
12
12
  "module": "src/index.js",
13
13
  "exports": {
@@ -53,5 +53,5 @@
53
53
  "vega-scale": "^7.1.1",
54
54
  "vega-util": "^1.16.0"
55
55
  },
56
- "gitHead": "05074d61428fe6c11c3eaa4e56796f830e794579"
56
+ "gitHead": "c8fe26f3421a1bec7323428f0bd7da46cda34dba"
57
57
  }
package/src/genomeSpy.js CHANGED
@@ -378,6 +378,7 @@ export default class GenomeSpy {
378
378
  // Now that all data have been loaded, the domains may need adjusting
379
379
  this.viewRoot.visit((view) => {
380
380
  for (const resolution of Object.values(view.resolutions.scale)) {
381
+ // TODO: Don't reconfigure multiple times
381
382
  // IMPORTANT TODO: Check that discrete domains and indexers match!!!!!!!!!
382
383
  resolution.reconfigure();
383
384
  }
@@ -9,7 +9,12 @@ import {
9
9
  } from "twgl.js";
10
10
  import { isArray, isString } from "vega-util";
11
11
 
12
- import { isDiscrete, isDiscretizing, isInterpolating } from "vega-scale";
12
+ import {
13
+ isContinuous,
14
+ isDiscrete,
15
+ isDiscretizing,
16
+ isInterpolating,
17
+ } from "vega-scale";
13
18
  import {
14
19
  createDiscreteColorTexture,
15
20
  createDiscreteTexture,
@@ -321,43 +326,53 @@ export default class WebGLHelper {
321
326
  const props = resolution.getScaleProps();
322
327
 
323
328
  const scale = resolution.getScale();
329
+ const range = /** @type {any[]} */ (scale.range());
324
330
 
325
331
  /** @type {WebGLTexture} */
326
332
  let texture;
327
333
 
328
334
  if (props.scheme) {
329
- let count = isString(props.scheme)
330
- ? undefined
331
- : props.scheme.count;
332
-
333
- count = fixCount(count, scale);
334
-
335
- texture = createSchemeTexture(
336
- props.scheme,
337
- this.gl,
338
- count,
339
- existingTexture
340
- );
341
- } else {
342
- // No scheme, assume that colors are specified in the range
343
-
344
- const range = /** @type {any[]} */ (scale.range());
345
-
346
- if (isInterpolating(scale.type)) {
347
- texture = createInterpolatedColorTexture(
335
+ if (scale.type == "threshold" && range) {
336
+ // Scale initialization may have configured the range. Let's use it.
337
+ texture = createDiscreteColorTexture(
348
338
  range,
349
- props.interpolate,
350
339
  this.gl,
340
+ scale.domain().length,
351
341
  existingTexture
352
342
  );
353
343
  } else {
354
- texture = createDiscreteColorTexture(
355
- range,
344
+ let count = isString(props.scheme)
345
+ ? undefined
346
+ : props.scheme.count;
347
+
348
+ count = fixCount(count, scale);
349
+
350
+ texture = createSchemeTexture(
351
+ props.scheme,
356
352
  this.gl,
357
- scale.domain().length,
353
+ count,
358
354
  existingTexture
359
355
  );
360
356
  }
357
+ } else if (
358
+ // Interpolating
359
+ isInterpolating(scale.type) ||
360
+ // Or piecewise
361
+ (isContinuous(scale.type) && range.length > 2)
362
+ ) {
363
+ texture = createInterpolatedColorTexture(
364
+ range,
365
+ props.interpolate,
366
+ this.gl,
367
+ existingTexture
368
+ );
369
+ } else {
370
+ texture = createDiscreteColorTexture(
371
+ range,
372
+ this.gl,
373
+ scale.domain().length,
374
+ existingTexture
375
+ );
361
376
  }
362
377
 
363
378
  this.rangeTextures.set(resolution, texture);
package/src/marks/mark.js CHANGED
@@ -271,7 +271,16 @@ export default class Mark {
271
271
  getSampleFacetMode() {
272
272
  if (this.encoders.facetIndex) {
273
273
  return SAMPLE_FACET_TEXTURE;
274
- } else if (this.unitView.getFacetAccessor()) {
274
+ } else if (
275
+ // If the UnitView is inside app's SampleView.
276
+ // TODO: This may break if non-faceted stuff is added to SampleView,
277
+ // e.g., view background or an x axis.
278
+ // This could also be more generic and work with other faceting views
279
+ // that will be available in the future.
280
+ [...this.unitView.getAncestors()].find(
281
+ (view) => "samples" in view.spec
282
+ )
283
+ ) {
275
284
  return SAMPLE_FACET_UNIFORM;
276
285
  }
277
286
  }
@@ -776,7 +785,14 @@ export default class Mark {
776
785
  }
777
786
  };
778
787
 
779
- const rangeEntry = this.rangeMap.get(options.facetId);
788
+ // If is either faceted or non-faceted, not both.
789
+ // An undefined key with vertices means that the mark is non-faceted.
790
+ // In such case, the same non-faceted data is repeated for each facet.
791
+ const facetId =
792
+ this.rangeMap.get(undefined).count == 0
793
+ ? options.facetId
794
+ : undefined;
795
+ const rangeEntry = this.rangeMap.get(facetId);
780
796
 
781
797
  return options.sampleFacetRenderingOptions
782
798
  ? function renderSampleFacetRange() {
@@ -314,7 +314,7 @@ export function generateScaleGlsl(channel, scale, channelDef) {
314
314
  if (piecewise) {
315
315
  // TODO: Handle range correctly. Now this assumes unit range.
316
316
  scaleBody.push(
317
- `transformed = (float(slot) + transformed) / (float(${name}.length()) - 1.0);`
317
+ `transformed = (float(slot) + transformed) / (float(${name}.length() - 1));`
318
318
  );
319
319
  }
320
320
  } else {
@@ -463,26 +463,40 @@ export function isHighPrecisionScale(type) {
463
463
  return type == "index" || type == "locus";
464
464
  }
465
465
 
466
+ // Maximum precise index number is 2^(23 + 11) ~ 17G
467
+ // Higher number increases precision but makes zooming unstable
468
+ const BS = 2 ** 11;
469
+ const BM = BS - 1;
470
+
466
471
  /**
467
- * @param {number} x
472
+ * @param {number} x Must be an integer
468
473
  * @param {number[]} [arr]
469
474
  */
470
- export function splitHighPrecision(x, arr) {
471
- // Maximum precise index number is 2^(23 + 11) ~ 17G
472
- // Higher number increases precision but makes zooming unstable
473
- const bs = 2 ** 11;
474
-
475
- const lo = x % bs;
476
- const hi = Math.round(x - lo);
477
- arr ??= [];
475
+ export function splitHighPrecision(x, arr = []) {
476
+ // Using a bitmask is MUCH faster than using modulo (at least on Chrome 112)
477
+ // https://www.wikiwand.com/en/Modulo#Performance_issues
478
+ const lo = x & BM;
479
+ const hi = x - lo;
480
+
478
481
  arr[0] = hi;
479
482
  arr[1] = lo;
483
+
480
484
  return arr;
481
485
  }
482
486
 
487
+ /**
488
+ * @param {number} x
489
+ */
490
+ function exactSplitHighPrecision(x) {
491
+ const lo = x % BS;
492
+ const hi = x - lo;
493
+
494
+ return [hi, lo];
495
+ }
496
+
483
497
  /**
484
498
  * @param {number[]} domain
485
499
  */
486
500
  export function toHighPrecisionDomainUniform(domain) {
487
- return [...splitHighPrecision(domain[0]), domain[1] - domain[0]];
501
+ return [...exactSplitHighPrecision(domain[0]), domain[1] - domain[0]];
488
502
  }
@@ -0,0 +1,180 @@
1
+ import { Type } from "./channel";
2
+ import { Data } from "./data";
3
+ import { Align, FontStyle, FontWeight } from "./font";
4
+ import { Scale } from "./scale";
5
+ import { LayerSpec, UnitSpec, ViewSpecBase } from "./view";
6
+
7
+ // TODO: Figure out how this could be moved to the app package and excluded
8
+ // from the core package.
9
+
10
+ /**
11
+ * A view specification for a SampleView.
12
+ * This is only functional in the GenomeSpy app.
13
+ */
14
+ export interface SampleSpec extends ViewSpecBase {
15
+ /**
16
+ * Sample metadata definition.
17
+ * If the object is empty, the sample identifiers will be inferred from the data.
18
+ */
19
+ samples: SampleDef;
20
+
21
+ /**
22
+ * The view specification to be repeated for each sample.
23
+ */
24
+ spec: LayerSpec | UnitSpec;
25
+
26
+ stickySummaries?: boolean;
27
+ }
28
+
29
+ export interface SampleAttributeDef {
30
+ /**
31
+ * The attribute type. One of `"nominal"`, `"ordinal"`, or `"quantitative"`.
32
+ */
33
+ type: Type; // TODO: Omit index/locus from available types
34
+
35
+ /**
36
+ * Scale definition for the (default) color channel
37
+ */
38
+ scale?: Scale;
39
+
40
+ /**
41
+ * An optional scale definition for mapping the attribute to
42
+ * the width of a metadata rectangle.
43
+ */
44
+ barScale?: Scale;
45
+
46
+ /**
47
+ * Width of the column in pixels.
48
+ */
49
+ width?: number;
50
+
51
+ /**
52
+ * Whether the attribute is visible by default.
53
+ */
54
+ visible?: boolean;
55
+ }
56
+
57
+ export interface SampleDef {
58
+ /**
59
+ * Optional metadata about the samples.
60
+ */
61
+ data?: Data;
62
+
63
+ /**
64
+ * Explicitly specify the sample attributes.
65
+ */
66
+ attributes?: Record<string, SampleAttributeDef>;
67
+
68
+ /**
69
+ * Text in the label title
70
+ *
71
+ * **Default:** `"Sample name"`
72
+ */
73
+ labelTitleText?: string;
74
+
75
+ /**
76
+ * How much space in pixels to reserve for the labels.
77
+ *
78
+ * **Default:** `140`
79
+ */
80
+ labelLength?: number;
81
+
82
+ /**
83
+ * The font typeface. GenomeSpy uses [SDF](https://github.com/Chlumsky/msdfgen)
84
+ * versions of [Google Fonts](https://fonts.google.com/). Check their
85
+ * availability at the [A-Frame
86
+ * Fonts](https://github.com/etiennepinchon/aframe-fonts/tree/master/fonts)
87
+ * repository. System fonts are **not** supported.
88
+ *
89
+ * **Default value:** `"Lato"`
90
+ */
91
+ labelFont?: string;
92
+
93
+ /**
94
+ * The font style. Valid values: `"normal"` and `"italic"`.
95
+ *
96
+ * **Default value:** `"normal"`
97
+ */
98
+ labelFontStyle?: FontStyle;
99
+
100
+ /**
101
+ * The font weight. The following strings and numbers are valid values:
102
+ * `"thin"` (`100`), `"light"` (`300`), `"regular"` (`400`),
103
+ * `"normal"` (`400`), `"medium"` (`500`), `"bold"` (`700`),
104
+ * `"black"` (`900`)
105
+ *
106
+ * **Default value:** `"regular"`
107
+ */
108
+ labelFontWeight?: FontWeight;
109
+
110
+ /**
111
+ * The font size in pixels.
112
+ *
113
+ * **Default value:** `11`
114
+ */
115
+ labelFontSize?: number;
116
+
117
+ /**
118
+ * The horizontal alignment of the text. One of `"left"`, `"center"`, or `"right"`.
119
+ *
120
+ * **Default value:** `"left"`
121
+ */
122
+ labelAlign?: Align;
123
+
124
+ /**
125
+ * Default size (width) of the metadata attribute columns.
126
+ * Can be configured per attribute using the `attributes` property.
127
+ *
128
+ * **Default value:** `10`
129
+ */
130
+ attributeSize?: number;
131
+
132
+ /**
133
+ * The font typeface. GenomeSpy uses [SDF](https://github.com/Chlumsky/msdfgen)
134
+ * versions of [Google Fonts](https://fonts.google.com/). Check their
135
+ * availability at the [A-Frame
136
+ * Fonts](https://github.com/etiennepinchon/aframe-fonts/tree/master/fonts)
137
+ * repository. System fonts are **not** supported.
138
+ *
139
+ * **Default value:** `"Lato"`
140
+ */
141
+ attributeLabelFont?: string;
142
+
143
+ /**
144
+ * The font style. Valid values: `"normal"` and `"italic"`.
145
+ *
146
+ * **Default value:** `"normal"`
147
+ */
148
+ attributeLabelFontStyle?: FontStyle;
149
+
150
+ /**
151
+ * The font weight. The following strings and numbers are valid values:
152
+ * `"thin"` (`100`), `"light"` (`300`), `"regular"` (`400`),
153
+ * `"normal"` (`400`), `"medium"` (`500`), `"bold"` (`700`),
154
+ * `"black"` (`900`)
155
+ *
156
+ * **Default value:** `"regular"`
157
+ */
158
+ attributeLabelFontWeight?: FontWeight;
159
+
160
+ /**
161
+ * The font size in pixels.
162
+ *
163
+ * **Default value:** `11`
164
+ */
165
+ attributeLabelFontSize?: number;
166
+
167
+ /**
168
+ * Angle to be added to the default label angle (-90).
169
+ *
170
+ * **Default value:** `0`
171
+ */
172
+ attributeLabelAngle?: number;
173
+
174
+ /**
175
+ * Spacing between attribute columns in pixels.
176
+ *
177
+ * **Default value:** `1`
178
+ */
179
+ attributeSpacing?: number;
180
+ }
@@ -156,14 +156,14 @@ export interface StackParams extends TransformParamsBase {
156
156
  * Cardinality, e.g., the number if distinct bases or amino acids. Used for
157
157
  * information content calculation when the offset is `"information"`.
158
158
  *
159
- * **Default:** `4`;
159
+ * **Default:** `4`
160
160
  */
161
161
  cardinality?: number;
162
162
 
163
163
  /**
164
164
  * The field that contains the base or amino acid. Used for
165
165
  * information content calculation when the offset is `"information"`.
166
- * The data items that have `null` in the baseField are considered gaps
166
+ * The data objects that have `null` in the baseField are considered gaps
167
167
  * and they are taken into account when scaling the the locus' information
168
168
  * content.
169
169
  */
@@ -1,12 +1,10 @@
1
1
  import { Data } from "./data";
2
- import { Scale } from "./scale";
3
2
  import { TransformParams } from "./transform";
4
3
  import {
5
4
  Channel,
6
5
  Encoding,
7
6
  FacetFieldDef,
8
7
  PrimaryPositionalChannel,
9
- Type,
10
8
  } from "./channel";
11
9
  import {
12
10
  FillAndStrokeProps,
@@ -15,6 +13,7 @@ import {
15
13
  RectProps,
16
14
  } from "./mark";
17
15
  import { Title } from "./title";
16
+ import { SampleSpec } from "./sampleView";
18
17
 
19
18
  export interface SizeDef {
20
19
  /** Size in pixels */
@@ -137,27 +136,6 @@ export interface FacetSpec extends ViewSpecBase {
137
136
  spacing?: number;
138
137
  }
139
138
 
140
- export interface SampleAttributeDef {
141
- type: Type; // TODO: Omit index/locus
142
- /** Color scale (primary) */
143
- scale?: Scale;
144
- barScale?: Scale;
145
- width?: number;
146
- visible?: boolean;
147
- }
148
-
149
- export interface SampleDef {
150
- data?: Data;
151
- attributes?: Record<string, SampleAttributeDef>;
152
- }
153
-
154
- export interface SampleSpec extends ViewSpecBase {
155
- samples: SampleDef;
156
- spec: LayerSpec | UnitSpec;
157
-
158
- stickySummaries?: boolean;
159
- }
160
-
161
139
  export type ResolutionTarget = "scale" | "axis";
162
140
 
163
141
  /**
@@ -30,20 +30,24 @@ export default async function dataTooltipHandler(datum, mark, params) {
30
30
  return "";
31
31
  };
32
32
 
33
+ const strippedEntries = Object.entries(datum).filter(
34
+ ([key, _value]) => !key.startsWith("_")
35
+ );
36
+
37
+ if (strippedEntries.length === 0) {
38
+ return;
39
+ }
40
+
33
41
  const table = html`
34
42
  <table class="attributes">
35
- ${Object.entries(datum)
36
- .filter(([key, value]) => !key.startsWith("_"))
37
- .map(
38
- ([key, value]) => html`
39
- <tr>
40
- <th>${key}</th>
41
- <td>
42
- ${formatObject(value)} ${legend(key, datum)}
43
- </td>
44
- </tr>
45
- `
46
- )}
43
+ ${strippedEntries.map(
44
+ ([key, value]) => html`
45
+ <tr>
46
+ <th>${key}</th>
47
+ <td>${formatObject(value)} ${legend(key, datum)}</td>
48
+ </tr>
49
+ `
50
+ )}
47
51
  </table>
48
52
  `;
49
53
 
@@ -527,7 +527,11 @@ export default class GridView extends ContainerView {
527
527
  return;
528
528
  }
529
529
 
530
- coords = coords.shrink(this.getPadding()); // TODO: Only applicable at view root
530
+ if (!this.parent) {
531
+ // Usually padding is applied by the parent GridView, but if this is the root view, we need to apply it here
532
+ coords = coords.shrink(this.getPadding());
533
+ }
534
+
531
535
  context.pushView(this, coords);
532
536
 
533
537
  const flexOpts = {
@@ -725,7 +729,7 @@ export default class GridView extends ContainerView {
725
729
  * @param {import("../spec/view").ViewBackground} viewBackground
726
730
  * @returns {import("../spec/view").UnitSpec}
727
731
  */
728
- function createBackground(viewBackground) {
732
+ export function createBackground(viewBackground) {
729
733
  return {
730
734
  configurableVisibility: false,
731
735
  data: { values: [{}] },