Skip to content

Commit 181fa0a

Browse files
committed
allow subfield access
1 parent 84d6a54 commit 181fa0a

File tree

13 files changed

+346
-149
lines changed

13 files changed

+346
-149
lines changed

dev/svelte/SwitchPositions.svelte

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,88 @@
11
<script>
2+
import {
3+
makeData,
4+
Float32,
5+
Vector,
6+
vectorFromArray,
7+
Struct,
8+
makeVector,
9+
Field,
10+
} from 'apache-arrow';
11+
212
export let scatterplot;
313
14+
let positionNum = 0;
415
async function click() {
5-
console.log(scatterplot.prefs.encoding.x)
16+
for (let i = 0; i < 10; i++) {
17+
if (scatterplot.deeptable.transformations['struct' + i]) {
18+
continue;
19+
}
20+
scatterplot.deeptable.transformations['struct' + i] = async function (
21+
tile,
22+
) {
23+
// Create a nested struct with a change.
24+
const x = (await tile.get_column('x')).toArray();
25+
const y = (await tile.get_column('y')).toArray();
26+
27+
const x_ = new Float32Array(x.length);
28+
const y_ = new Float32Array(y.length);
29+
30+
for (let i = 0; i < x.length; i++) {
31+
const r = (Math.random() + Math.random()) / 3;
32+
const theta = Math.random() * Math.PI * 2;
33+
x_[i] = x[i] + Math.cos(theta) * r;
34+
y_[i] = y[i] + Math.sin(theta) * r;
35+
}
36+
37+
const d = makeData({
38+
type: new Struct([
39+
new Field('x', new Float32()),
40+
new Field('y', new Float32()),
41+
]),
42+
children: [vectorFromArray(x_).data[0], vectorFromArray(y_).data[0]],
43+
});
44+
const r = new Vector([d]);
45+
return r;
46+
};
47+
48+
scatterplot.deeptable.map((d) => d.get_column('struct' + i));
49+
}
50+
await new Promise((resolve) => {
51+
setTimeout(() => resolve());
52+
}, 100);
53+
let r = 'struct' + (positionNum++ % 10);
654
await scatterplot.plotAPI({
55+
duration: 1000,
756
encoding: {
857
x: {
9-
field: scatterplot.prefs.encoding.x.field === 'x' ? 'y' : 'x',
10-
transform: scatterplot.prefs.encoding.x.field === 'x' ? 'linear': 'literal'
58+
field: r,
59+
subfield: ['x'],
60+
transform: 'literal',
61+
domain: [-10, 10],
1162
},
1263
y: {
13-
field: scatterplot.prefs.encoding.y.field === 'y' ? 'x' : 'y',
14-
transform: scatterplot.prefs.encoding.y.field === 'y' ? 'linear': 'literal'
15-
}
16-
}
17-
})
64+
field: r,
65+
subfield: ['y'],
66+
transform: 'literal',
67+
domain: [-10, 10],
68+
},
69+
},
70+
});
71+
// await scatterplot.plotAPI({
72+
// encoding: {
73+
// x: {
74+
// field: scatterplot.prefs.encoding.x.field === 'x' ? 'y' : 'x',
75+
// transform:
76+
// scatterplot.prefs.encoding.x.field === 'x' ? 'linear' : 'literal',
77+
// },
78+
// y: {
79+
// field: scatterplot.prefs.encoding.y.field === 'y' ? 'x' : 'y',
80+
// transform:
81+
// scatterplot.prefs.encoding.y.field === 'y' ? 'linear' : 'literal',
82+
// },
83+
// },
84+
// });
1885
}
1986
</script>
2087

21-
<button on:click={click}>
22-
Switch positions
23-
</button>
88+
<button on:click={click}> Switch positions </button>

src/Deeptable.ts

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
Int32,
2525
Int8,
2626
tableToIPC,
27+
Struct,
2728
} from 'apache-arrow';
2829
import { Scatterplot } from './scatterplot';
2930
import { wrapArrowTable } from './wrap_arrow';
@@ -34,6 +35,8 @@ import type {
3435
IdSelectParams,
3536
} from './selection';
3637
import { DataSelection } from './selection';
38+
import { Some, TupleMap } from './utilityFunctions';
39+
import { getNestedVector } from './regl_rendering';
3740

3841
type TransformationStatus = 'queued' | 'in progress' | 'complete' | 'failed';
3942

@@ -70,7 +73,8 @@ export class Deeptable {
7073
...defaultTransformations,
7174
};
7275
public _plot: Scatterplot | null;
73-
private extents: Record<string, [number, number] | [Date, Date]> = {};
76+
private extents: TupleMap<string, [number, number] | [Date, Date]> =
77+
new TupleMap();
7478
// A 3d identifier for the tile. Usually [z, x, y]
7579
private _extent?: Rectangle;
7680
public _ix_seed = 0;
@@ -134,6 +138,9 @@ export class Deeptable {
134138
this.root_tile = new Tile(defaultManifest, null, this);
135139
const preProcessRootTile = this.root_tile.preprocessRootTileInfo();
136140

141+
// At instantiation, the deeptable isn't ready; only once this
142+
// async stuff is done can the deeptable be used.
143+
// TODO: Add an async static method as the preferred initialization method.
137144
this.promise = preProcessRootTile.then(async () => {
138145
const batch = await this.root_tile.get_arrow(null);
139146
const schema = batch.schema;
@@ -341,13 +348,24 @@ export class Deeptable {
341348

342349
domain<T extends [number, number] | [string, Date] = [number, number]>(
343350
columnName: string,
351+
subfield?: string[],
344352
): [T[1], T[1]] {
345-
if (this.extents[columnName]) {
346-
return this.extents[columnName];
353+
const key = [columnName, ...(subfield || [])] as Some<string>;
354+
if (this.extents.get(key)) {
355+
return this.extents.get(key);
356+
}
357+
358+
// First -- look at the schema metadata.
359+
let dim = this._schema?.fields.find((d) => d.name === columnName);
360+
for (const sub in subfield) {
361+
if (dim === undefined) {
362+
continue;
363+
}
364+
console.log({ dim });
365+
dim = (dim as Field<Struct<any>>).type.children.find(
366+
(d) => d.name === sub,
367+
);
347368
}
348-
const dim = this._schema?.fields.find(
349-
(d) => d.name === columnName,
350-
) as Field<DS.SupportedArrowTypes>;
351369
if (dim !== undefined) {
352370
let min: T[0] | undefined = undefined;
353371
let max: T[0] | undefined = undefined;
@@ -370,24 +388,30 @@ export class Deeptable {
370388
'Date field extents in metadata must be passed as strings',
371389
);
372390
}
373-
return (this.extents[columnName] = [new Date(min), new Date(max)]);
391+
this.extents.set(key, [new Date(min), new Date(max)]);
392+
return this.extents.get(key);
374393
}
375394
if (typeof max === 'string') {
376395
throw new Error('Failed to parse min-max as numbers');
377396
}
378397
if (min !== undefined) {
379-
return (this.extents[columnName] = [min as T[1], max as T[1]] as
398+
this.extents.set(key, [min as T[1], max as T[1]] as
380399
| [number, number]
381400
| [Date, Date]);
401+
return this.extents.get(key);
382402
}
383403
}
404+
384405
const vectors: Vector[] = this.map((tile) => tile)
385406
.filter((d) => d.hasLoadedColumn(columnName))
386-
.map((d) => d.record_batch.getChild(columnName) as Vector<Float32>);
407+
.map((d) => getNestedVector(d, [columnName, ...(subfield || [])]));
408+
387409
const extented = extent([...new Vector(vectors)]) as [T[1], T[1]] as
388410
| [number, number]
389411
| [Date, Date];
390-
return (this.extents[columnName] = extented);
412+
413+
this.extents.set(key, extented);
414+
return this.extents.get(key);
391415
}
392416

393417
*points(bbox: Rectangle | undefined, max_ix = 1e99) {

src/aesthetics/Aesthetic.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import type { TextureSet } from './AestheticSet';
22
import { isConstantChannel } from '../typing';
3-
import { Type, Vector } from 'apache-arrow';
3+
import { Struct, Type, Vector } from 'apache-arrow';
44
import { StructRowProxy } from 'apache-arrow/row/struct';
55
import { isNumber } from 'lodash';
66
import type * as DS from '../types';
77
import { Scatterplot } from '../scatterplot';
8+
import { Some } from '../utilityFunctions';
89

910
/**
1011
* An Aesthetic bundles all operations in mapping from user dataspace to webGL based aesthetics.
@@ -26,6 +27,7 @@ export abstract class Aesthetic<
2627
public abstract default_range: [Output['rangeType'], Output['rangeType']];
2728
public scatterplot: Scatterplot;
2829
public field: string | null = null;
30+
public subfield: string[] = [];
2931
public _texture_buffer: Float32Array | Uint8Array | null = null;
3032
protected abstract _func?: (d: Input['domainType']) => Output['rangeType'];
3133
public aesthetic_map: TextureSet;
@@ -76,9 +78,25 @@ export abstract class Aesthetic<
7678
this.field = null;
7779
} else {
7880
this.field = encoding.field;
81+
if (encoding.subfield) {
82+
this.subfield = Array.isArray(encoding.subfield)
83+
? encoding.subfield
84+
: [encoding.subfield];
85+
}
7986
}
8087
}
8188

89+
/**
90+
* Returns the keys that are used to access the data in the record batch,
91+
* including with any nesting.
92+
*/
93+
get columnKeys(): null | Some<string> {
94+
if (this.field === null) {
95+
return null;
96+
}
97+
return [this.field, ...this.subfield] as Some<string>;
98+
}
99+
82100
get deeptable() {
83101
return this.scatterplot.deeptable;
84102
}
@@ -100,10 +118,14 @@ export abstract class Aesthetic<
100118

101119
value_for(point: Datum): Input['domainType'] | null {
102120
if (this.field && point[this.field]) {
103-
return point[this.field] as Input['domainType'];
121+
let v = point[this.field] as Input['domainType'];
122+
for (let i = 0; i < this.subfield.length; i++) {
123+
v = v[this.subfield[i]] as Input['domainType'];
124+
}
125+
return v;
126+
// Needs a default perhaps?
127+
return null;
104128
}
105-
// Needs a default perhaps?
106-
return null;
107129
}
108130

109131
get map_position() {
@@ -136,9 +158,17 @@ export abstract class Aesthetic<
136158
if (this.field === null || this.field === undefined) {
137159
return (this.column = null);
138160
}
139-
return (this.column = this.deeptable.root_tile.record_batch.getChild(
140-
this.field,
141-
) as Vector<Input['arrowType']>);
161+
let output: Vector<Input['arrowType']> | Vector<Struct> | null = null;
162+
for (const f of [this.field, ...this.subfield]) {
163+
if (output === null) {
164+
output = this.deeptable.root_tile.record_batch.getChild(f) as Vector<
165+
Input['arrowType']
166+
>;
167+
} else {
168+
output = (output as Vector<Struct>).getChild(f) as Vector<Struct>;
169+
}
170+
}
171+
return (this.column = output as Vector<Input['arrowType']>);
142172
}
143173

144174
is_dictionary(): boolean {

src/aesthetics/AestheticSet.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { Deeptable } from '../Deeptable';
66
import { StatefulAesthetic } from './StatefulAesthetic';
77
import type { Encoding } from '../types';
88
import type * as DS from '../types';
9+
import { TupleSet } from '../utilityFunctions';
910

1011
type AesMap = {
1112
[K in keyof typeof dimensions]: StatefulAesthetic<
@@ -83,6 +84,12 @@ export class AestheticSet {
8384
}
8485
}
8586

87+
_neededFields: TupleSet<string> = new TupleSet();
88+
89+
get neededFields(): string[][] {
90+
return [...this._neededFields.values()];
91+
}
92+
8693
apply_encoding(encoding: Encoding) {
8794
if (
8895
encoding['jitter_radius'] &&
@@ -107,6 +114,17 @@ export class AestheticSet {
107114
this.dim(k).update(encoding[k] as DS.ChannelType | null);
108115
}
109116

117+
// Update the needed fields.
118+
this._neededFields.clear();
119+
120+
for (const v of Object.values(this.store)) {
121+
if (v instanceof StatefulAesthetic) {
122+
for (const f of v.neededFields) {
123+
this._neededFields.add(f);
124+
}
125+
}
126+
}
127+
110128
// Apply settings that are not full-on aesthetics.
111129
for (const setting of ['jitter_method'] as const) {
112130
this.options[setting].last = this.options[setting].current;

src/aesthetics/ScaledAesthetic.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ export abstract class ScaledAesthetic<
203203
Input['domainType'],
204204
];
205205
} else {
206-
return this.scatterplot.deeptable.domain(this.field);
206+
return this.scatterplot.deeptable.domain(this.field, this.subfield);
207207
}
208208
}
209209

src/aesthetics/StatefulAesthetic.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export type ConcreteScaledAesthetic =
5555
import type { Deeptable } from '../Deeptable';
5656
import type { Regl } from 'regl';
5757
import type { TextureSet } from './AestheticSet';
58+
import { Some } from '../utilityFunctions';
5859

5960
export class StatefulAesthetic<T extends ConcreteAesthetic> {
6061
/**
@@ -97,11 +98,12 @@ export class StatefulAesthetic<T extends ConcreteAesthetic> {
9798
] as [T, T];
9899
}
99100

100-
get neededFields(): string[] {
101-
return [this.current.field, this.last.field].filter(
101+
get neededFields(): Some<string>[] {
102+
return [this.current.columnKeys, this.last.columnKeys].filter(
102103
(f) => f !== null,
103-
) as string[];
104+
);
104105
}
106+
105107
get current() {
106108
return this.states[0];
107109
}

0 commit comments

Comments
 (0)