@malloydata/render 0.0.423 → 0.0.425
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/module/index.mjs +17074 -17021
- package/dist/module/index.umd.js +308 -283
- package/docs/plans/field-creation-analysis.md +178 -0
- package/docs/plugin-api-reference.md +238 -0
- package/docs/plugin-quick-start.md +103 -0
- package/docs/plugin-system.md +321 -0
- package/docs/renderer_tag_cheatsheet.md +72 -0
- package/docs/renderer_tags_overview.md +916 -0
- package/docs/testing.md +67 -0
- package/docs/validation.md +139 -0
- package/package.json +8 -8
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Field Creation Analysis: Excessive Field Creation and Tag Parsing in malloy-render
|
|
2
|
+
|
|
3
|
+
## Problem Statement
|
|
4
|
+
|
|
5
|
+
In the malloy-render package, we're experiencing significant performance overhead during data visualization preprocessing. For a simple two-column Malloy query like:
|
|
6
|
+
|
|
7
|
+
```malloy
|
|
8
|
+
view: two_column is {
|
|
9
|
+
select: brand, department
|
|
10
|
+
limit: 1
|
|
11
|
+
}
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
We expect to see:
|
|
15
|
+
- 2 Field objects created (one for each column)
|
|
16
|
+
- 2 tagFor calls (one for each field's metadata parsing)
|
|
17
|
+
|
|
18
|
+
However, we're actually seeing:
|
|
19
|
+
- **5 Field objects created** (2.5x expected)
|
|
20
|
+
- **7-8 tagFor calls** (3.5-4x expected)
|
|
21
|
+
|
|
22
|
+
Since tag parsing is an expensive operation (parsing annotations and metadata), this overhead becomes significant with large datasets. For example, with 5000 rows containing nested data, this can lead to hundreds of thousands of unnecessary field creations and tag parsing operations.
|
|
23
|
+
|
|
24
|
+
## Root Cause Analysis
|
|
25
|
+
|
|
26
|
+
### The Problem Flow
|
|
27
|
+
|
|
28
|
+
1. **RootField Creation** (extends RepeatedRecordField)
|
|
29
|
+
- Creates RootField instance → 1 tagFor call for RootField itself
|
|
30
|
+
|
|
31
|
+
2. **ArrayField Constructor** (line 27 in nest.ts)
|
|
32
|
+
- Creates an `elementField` for the array's element type
|
|
33
|
+
- This creates a RecordField named "element" → 1 Field.from call + 1 tagFor call
|
|
34
|
+
|
|
35
|
+
3. **RecordField Constructor** (line 159 in nest.ts)
|
|
36
|
+
- Creates fields for brand and department as children of "element"
|
|
37
|
+
- 2 Field.from calls + 2 tagFor calls
|
|
38
|
+
|
|
39
|
+
4. **RepeatedRecordField Constructor** (line 54 in nest.ts)
|
|
40
|
+
- **DUPLICATES** the field creation for brand and department
|
|
41
|
+
- Creates them again as direct children of RepeatedRecordField
|
|
42
|
+
- 2 more Field.from calls + 2 more tagFor calls
|
|
43
|
+
|
|
44
|
+
5. **Additional RecordField Creation** (lines 58-71 in nest.ts)
|
|
45
|
+
- Creates a `nestedRecordField` instance
|
|
46
|
+
- 1 more tagFor call
|
|
47
|
+
|
|
48
|
+
### The Duplication Issue
|
|
49
|
+
|
|
50
|
+
The main issue is that **RepeatedRecordField creates fields TWICE**:
|
|
51
|
+
|
|
52
|
+
1. First in ArrayField's constructor via `elementField`
|
|
53
|
+
2. Then again in RepeatedRecordField's own constructor at line 54
|
|
54
|
+
|
|
55
|
+
This happens because:
|
|
56
|
+
- ArrayField creates an elementField (RecordField) which creates brand/department fields
|
|
57
|
+
- RepeatedRecordField then directly parses the record type and creates brand/department fields again
|
|
58
|
+
- The `nestedRecordField` creation adds another layer of overhead
|
|
59
|
+
|
|
60
|
+
## Current Architecture Analysis
|
|
61
|
+
|
|
62
|
+
### Why the Extra Fields Exist
|
|
63
|
+
|
|
64
|
+
1. **ArrayField's `elementField`**
|
|
65
|
+
- **Purpose**: Provides schema for array elements (could be any type: string, number, record, etc.)
|
|
66
|
+
- **Used by**: ArrayCell to create appropriate cell types for each array element
|
|
67
|
+
- **Necessity**: Required for generic array handling
|
|
68
|
+
|
|
69
|
+
2. **RepeatedRecordField's Triple Structure**
|
|
70
|
+
- **`elementField`** (inherited from ArrayField): Generic element schema
|
|
71
|
+
- **`fields` array**: Direct access to record fields for performance
|
|
72
|
+
- **`nestedRecordField`**: RecordField instance for cell creation compatibility
|
|
73
|
+
|
|
74
|
+
3. **The Architecture's Intent**
|
|
75
|
+
|
|
76
|
+
The design appears to be intentional for several reasons:
|
|
77
|
+
- **Type Safety**: Each field type knows exactly what kind of data it contains
|
|
78
|
+
- **Performance**: Direct field access via `fields` array avoids indirection
|
|
79
|
+
- **Compatibility**: The `nestedRecordField` allows RepeatedRecordField to work seamlessly with RecordCell
|
|
80
|
+
|
|
81
|
+
### Why This Architecture Evolved
|
|
82
|
+
|
|
83
|
+
1. **Type System Alignment**: Mirrors Malloy's type system where arrays and records are distinct
|
|
84
|
+
2. **Performance Optimization**: Direct field access without indirection
|
|
85
|
+
3. **API Compatibility**: RecordCell expects a RecordField, not a RepeatedRecordField
|
|
86
|
+
4. **Incremental Development**: Features added over time without refactoring base structures
|
|
87
|
+
|
|
88
|
+
## Rearchitecture Proposals
|
|
89
|
+
|
|
90
|
+
### Option 1: Lazy Field Creation (Minimal Change)
|
|
91
|
+
**Concept**: Create fields only when accessed, cache aggressively
|
|
92
|
+
|
|
93
|
+
**Pros**:
|
|
94
|
+
- Minimal API changes
|
|
95
|
+
- Reduces upfront cost
|
|
96
|
+
- Fields created only once
|
|
97
|
+
|
|
98
|
+
**Cons**:
|
|
99
|
+
- Doesn't eliminate architectural duplication
|
|
100
|
+
- Just defers the problem
|
|
101
|
+
|
|
102
|
+
### Option 2: Unified Field Registry (Medium Change)
|
|
103
|
+
**Concept**: Single source of truth for all fields with automatic deduplication
|
|
104
|
+
|
|
105
|
+
**Pros**:
|
|
106
|
+
- Single point of field creation
|
|
107
|
+
- Automatic deduplication
|
|
108
|
+
- Easy to add metrics/debugging
|
|
109
|
+
|
|
110
|
+
**Cons**:
|
|
111
|
+
- Requires passing registry throughout
|
|
112
|
+
- May complicate field lifecycle
|
|
113
|
+
|
|
114
|
+
### Option 3: Rethink Inheritance (Major Change)
|
|
115
|
+
**Concept**: RepeatedRecordField shouldn't extend ArrayField since it's conceptually a table, not an array
|
|
116
|
+
|
|
117
|
+
**Pros**:
|
|
118
|
+
- Eliminates conceptual mismatch
|
|
119
|
+
- No duplicate fields
|
|
120
|
+
- Cleaner mental model
|
|
121
|
+
|
|
122
|
+
**Cons**:
|
|
123
|
+
- Breaking API change
|
|
124
|
+
- Significant refactoring needed
|
|
125
|
+
|
|
126
|
+
### Option 4: Cell-Centric Architecture (Revolutionary)
|
|
127
|
+
**Concept**: Fields become lightweight metadata, cells handle complexity and create child fields on demand
|
|
128
|
+
|
|
129
|
+
**Pros**:
|
|
130
|
+
- Fields created only when data is processed
|
|
131
|
+
- Minimal memory for schema
|
|
132
|
+
- Natural lazy evaluation
|
|
133
|
+
|
|
134
|
+
**Cons**:
|
|
135
|
+
- Major paradigm shift
|
|
136
|
+
- May impact performance differently
|
|
137
|
+
|
|
138
|
+
## Recommendation
|
|
139
|
+
|
|
140
|
+
**Short term**: Implement the current approach with `skipFieldCreation` flag. This solves the immediate performance issue by:
|
|
141
|
+
- Creating fields once in RepeatedRecordField constructor
|
|
142
|
+
- Sharing these fields with the nestedRecordField
|
|
143
|
+
- Preventing RecordField from creating its own duplicate fields
|
|
144
|
+
|
|
145
|
+
**Long term**: Consider **Option 3 (Rethink Inheritance)**. The fundamental issue is that RepeatedRecordField is conceptually not an array - it's a table. The inheritance from ArrayField creates unnecessary complexity.
|
|
146
|
+
|
|
147
|
+
The ideal architecture would:
|
|
148
|
+
1. Create each field exactly once
|
|
149
|
+
2. Share field instances where semantically equivalent
|
|
150
|
+
3. Align with the mental model of tables vs arrays
|
|
151
|
+
4. Minimize memory usage for large datasets
|
|
152
|
+
|
|
153
|
+
## Impact
|
|
154
|
+
|
|
155
|
+
For the example two-column query, fixing this would reduce:
|
|
156
|
+
- Field creation from 5 to 2 (60% reduction)
|
|
157
|
+
- tagFor calls from 7-8 to 2 (71-75% reduction)
|
|
158
|
+
|
|
159
|
+
For larger datasets with nested structures, this optimization becomes even more critical, potentially eliminating hundreds of thousands of unnecessary operations.
|
|
160
|
+
|
|
161
|
+
## Implementation Status
|
|
162
|
+
|
|
163
|
+
### Completed (Short-term Fix)
|
|
164
|
+
✅ **Lazy elementField creation in ArrayField**: The elementField is now created only when accessed, preventing duplicate field creation for RepeatedRecordField instances.
|
|
165
|
+
|
|
166
|
+
✅ **Field sharing in RepeatedRecordField**: The RepeatedRecordField now shares its fields with both the nestedRecordField and the elementField (when accessed), eliminating duplicate Field.from calls.
|
|
167
|
+
|
|
168
|
+
✅ **Skip redundant tag parsing**: Added skipTagParsing parameter to prevent unnecessary tagFor calls for the synthetic RecordField instance.
|
|
169
|
+
|
|
170
|
+
**Results achieved**:
|
|
171
|
+
- Field.from calls reduced from 5 to 2 ✓
|
|
172
|
+
- tagFor calls reduced from 7-8 to 4 (further optimization possible)
|
|
173
|
+
|
|
174
|
+
### Future Work (Long-term Architecture)
|
|
175
|
+
The long-term architectural improvements (Option 3: Rethink Inheritance) remain as future work. This would involve:
|
|
176
|
+
- Redesigning RepeatedRecordField to not extend ArrayField
|
|
177
|
+
- Creating a more appropriate inheritance hierarchy that reflects the conceptual difference between arrays and tables
|
|
178
|
+
- Potentially achieving the ideal 2 tagFor calls by eliminating all redundant metadata parsing
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Plugin API Reference
|
|
2
|
+
|
|
3
|
+
## Core Types
|
|
4
|
+
|
|
5
|
+
### RenderPluginFactory<TInstance>
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
interface RenderPluginFactory<TInstance extends RenderPluginInstance> {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
getValidationSpec?(): RendererValidationSpec;
|
|
11
|
+
matches(field: Field, fieldTag: Tag, fieldType: FieldType): boolean;
|
|
12
|
+
create(field: Field, pluginOptions?: unknown, modelTag?: Tag): TInstance;
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
#### Properties
|
|
17
|
+
- `name`: Unique identifier for the plugin
|
|
18
|
+
|
|
19
|
+
#### Methods
|
|
20
|
+
- `getValidationSpec()`: Declares which tag paths this plugin semantically owns, so the unread-tag detector does not false-warn on valid uses. Required for plugins that read tags at render time. See [validation.md](validation.md) for the ownership model.
|
|
21
|
+
|
|
22
|
+
- `matches()`: Determines if plugin should handle a field
|
|
23
|
+
- `field`: Field metadata and structure
|
|
24
|
+
- `fieldTag`: Field annotations/tags
|
|
25
|
+
- `fieldType`: Enumerated field type
|
|
26
|
+
- Returns: `boolean`
|
|
27
|
+
- Throw when the user's tag asks for this plugin but the field cannot support it — the user sees a red error tile. See [validation.md](validation.md) for the throw-vs-log rule.
|
|
28
|
+
|
|
29
|
+
- `create()`: Instantiates plugin for a matched field
|
|
30
|
+
- `field`: Field to render
|
|
31
|
+
- `pluginOptions`: Configuration from renderer options
|
|
32
|
+
- `modelTag`: Model-level annotations
|
|
33
|
+
- Returns: Plugin instance
|
|
34
|
+
- Do all tag reads here (setup time), not in `renderComponent()` or `beforeRender()` — setup-time reads are validated and marked as consumed automatically.
|
|
35
|
+
|
|
36
|
+
### RendererValidationSpec
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
interface RendererValidationSpec {
|
|
40
|
+
readonly renderer: string;
|
|
41
|
+
readonly ownedPaths?: string[][];
|
|
42
|
+
readonly childOwnedPaths?: string[][];
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Declares the tag paths a renderer owns — tags whose meaning depends on this renderer being active. See [validation.md](validation.md) for when to declare ownership.
|
|
47
|
+
|
|
48
|
+
### RenderPluginInstance
|
|
49
|
+
|
|
50
|
+
Base type: Union of `SolidJSRenderPluginInstance` and `DOMRenderPluginInstance`
|
|
51
|
+
|
|
52
|
+
#### Common Properties
|
|
53
|
+
```typescript
|
|
54
|
+
interface BaseRenderPluginInstance<TMetadata = unknown> {
|
|
55
|
+
readonly name: string;
|
|
56
|
+
readonly field: Field;
|
|
57
|
+
readonly sizingStrategy: 'fixed' | 'fill';
|
|
58
|
+
|
|
59
|
+
getMetadata(): TMetadata;
|
|
60
|
+
processData?(field: NestField, cell: NestCell): void;
|
|
61
|
+
beforeRender?(metadata: RenderMetadata, options: GetResultMetadataOptions): void;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### SolidJSRenderPluginInstance
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
interface SolidJSRenderPluginInstance<TMetadata = unknown>
|
|
69
|
+
extends BaseRenderPluginInstance<TMetadata> {
|
|
70
|
+
readonly renderMode: 'solidjs';
|
|
71
|
+
renderComponent(props: RenderProps): JSXElement;
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### DOMRenderPluginInstance
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
interface DOMRenderPluginInstance<TMetadata = unknown>
|
|
79
|
+
extends BaseRenderPluginInstance<TMetadata> {
|
|
80
|
+
readonly renderMode: 'dom';
|
|
81
|
+
renderToDOM(container: HTMLElement, props: RenderProps): void;
|
|
82
|
+
cleanup?(container: HTMLElement): void;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### CoreVizPluginInstance
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
interface CoreVizPluginMethods {
|
|
90
|
+
getSchema(): JSONSchemaObject;
|
|
91
|
+
getSettings(): Record<string, unknown>;
|
|
92
|
+
getDefaultSettings(): Record<string, unknown>;
|
|
93
|
+
settingsToTag(settings: Record<string, unknown>): Tag;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type CoreVizPluginInstance<TMetadata = unknown> =
|
|
97
|
+
SolidJSRenderPluginInstance<TMetadata> & CoreVizPluginMethods;
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Supporting Types
|
|
101
|
+
|
|
102
|
+
### RenderProps
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
interface RenderProps {
|
|
106
|
+
dataColumn: Cell;
|
|
107
|
+
field: Field;
|
|
108
|
+
customProps?: Record<string, unknown>;
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### FieldType Enum
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
enum FieldType {
|
|
116
|
+
String = 'string',
|
|
117
|
+
Number = 'number',
|
|
118
|
+
Boolean = 'boolean',
|
|
119
|
+
Date = 'date',
|
|
120
|
+
Timestamp = 'timestamp',
|
|
121
|
+
JSON = 'json',
|
|
122
|
+
Record = 'record',
|
|
123
|
+
RepeatedRecord = 'repeated_record',
|
|
124
|
+
SQLNative = 'sql_native'
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Field Interface (Key Methods)
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
interface Field {
|
|
132
|
+
name: string;
|
|
133
|
+
key: string;
|
|
134
|
+
tag: Tag;
|
|
135
|
+
|
|
136
|
+
isString(): boolean;
|
|
137
|
+
isNumber(): boolean;
|
|
138
|
+
isBoolean(): boolean;
|
|
139
|
+
isDate(): boolean;
|
|
140
|
+
isTime(): boolean;
|
|
141
|
+
isJSON(): boolean;
|
|
142
|
+
isNest(): boolean;
|
|
143
|
+
isBasic(): boolean;
|
|
144
|
+
|
|
145
|
+
// For nested fields
|
|
146
|
+
fields?: Field[];
|
|
147
|
+
fieldAt(path: string): Field | undefined;
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Cell Interface (Key Methods)
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
interface Cell {
|
|
155
|
+
value: any;
|
|
156
|
+
|
|
157
|
+
isNull(): boolean;
|
|
158
|
+
isString(): boolean;
|
|
159
|
+
isNumber(): boolean;
|
|
160
|
+
isBoolean(): boolean;
|
|
161
|
+
isDate(): boolean;
|
|
162
|
+
isTime(): boolean;
|
|
163
|
+
isJSON(): boolean;
|
|
164
|
+
isRepeatedRecord(): boolean;
|
|
165
|
+
|
|
166
|
+
// For nested cells
|
|
167
|
+
column(name: string): Cell;
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Tag Interface (Key Methods)
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
interface Tag {
|
|
175
|
+
has(key: string): boolean;
|
|
176
|
+
text(key: string): string | undefined;
|
|
177
|
+
boolean(key: string): boolean | undefined;
|
|
178
|
+
number(key: string): number | undefined;
|
|
179
|
+
json(key: string): any | undefined;
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Renderer Integration
|
|
184
|
+
|
|
185
|
+
### MalloyRendererOptions
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
interface MalloyRendererOptions {
|
|
189
|
+
plugins?: RenderPluginFactory[];
|
|
190
|
+
pluginOptions?: Record<string, unknown>;
|
|
191
|
+
// ... other options
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Plugin Registration
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
const renderer = new MalloyRenderer({
|
|
199
|
+
plugins: [Plugin1Factory, Plugin2Factory],
|
|
200
|
+
pluginOptions: {
|
|
201
|
+
'plugin1': { /* options */ },
|
|
202
|
+
'plugin2': { /* options */ }
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Utility Functions
|
|
208
|
+
|
|
209
|
+
### isCoreVizPluginInstance
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
function isCoreVizPluginInstance(
|
|
213
|
+
plugin: RenderPluginInstance
|
|
214
|
+
): plugin is CoreVizPluginInstance
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Checks if a plugin implements the core visualization interface.
|
|
218
|
+
|
|
219
|
+
## Lifecycle Methods
|
|
220
|
+
|
|
221
|
+
### processData(field, cell)
|
|
222
|
+
- Called during data processing phase
|
|
223
|
+
- Use for expensive calculations
|
|
224
|
+
- Results can be stored in plugin instance
|
|
225
|
+
|
|
226
|
+
### beforeRender(metadata, options)
|
|
227
|
+
- Called before rendering
|
|
228
|
+
- Access to full result metadata
|
|
229
|
+
- Can access Vega config overrides
|
|
230
|
+
|
|
231
|
+
### renderComponent(props) / renderToDOM(container, props)
|
|
232
|
+
- Called to render the visualization
|
|
233
|
+
- Access to current data cell and field metadata
|
|
234
|
+
|
|
235
|
+
### cleanup(container)
|
|
236
|
+
- DOM plugins only
|
|
237
|
+
- Called when visualization is removed
|
|
238
|
+
- Clean up event listeners, timers, etc.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Malloy Render Plugin Quick Start
|
|
2
|
+
|
|
3
|
+
## Minimal Plugin Example
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import type { RenderPluginFactory, SolidJSRenderPluginInstance } from '@/api/plugin-types';
|
|
7
|
+
|
|
8
|
+
export const MinimalPluginFactory: RenderPluginFactory<SolidJSRenderPluginInstance> = {
|
|
9
|
+
name: 'minimal',
|
|
10
|
+
|
|
11
|
+
matches: (field, fieldTag) => fieldTag.has('minimal'),
|
|
12
|
+
|
|
13
|
+
create: (field) => ({
|
|
14
|
+
name: 'minimal',
|
|
15
|
+
field,
|
|
16
|
+
renderMode: 'solidjs',
|
|
17
|
+
sizingStrategy: 'fixed',
|
|
18
|
+
|
|
19
|
+
renderComponent: (props) => (
|
|
20
|
+
<div>{props.dataColumn.value}</div>
|
|
21
|
+
),
|
|
22
|
+
|
|
23
|
+
getMetadata: () => ({ type: 'minimal' })
|
|
24
|
+
})
|
|
25
|
+
};
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Key Concepts
|
|
29
|
+
|
|
30
|
+
### 1. Factory Pattern
|
|
31
|
+
|
|
32
|
+
- **Factory** creates plugin instances
|
|
33
|
+
- **Instance** handles rendering
|
|
34
|
+
|
|
35
|
+
### 2. Matching Fields
|
|
36
|
+
|
|
37
|
+
Plugins match fields based on:
|
|
38
|
+
|
|
39
|
+
- Tags (e.g., `# my_plugin`)
|
|
40
|
+
- Field types (String, Number, RepeatedRecord, etc.)
|
|
41
|
+
- Custom logic
|
|
42
|
+
|
|
43
|
+
### 3. Render Modes
|
|
44
|
+
|
|
45
|
+
- **solidjs**: Reactive components (recommended)
|
|
46
|
+
- **dom**: Direct DOM manipulation
|
|
47
|
+
|
|
48
|
+
### 4. Sizing Strategies
|
|
49
|
+
|
|
50
|
+
- **fixed**: Plugin controls its size
|
|
51
|
+
- **fill**: Adapts to container
|
|
52
|
+
|
|
53
|
+
## Common Patterns
|
|
54
|
+
|
|
55
|
+
### Chart Plugin
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
matches: (field, fieldTag, fieldType) => {
|
|
59
|
+
return fieldType === FieldType.RepeatedRecord && fieldTag.has('my_chart');
|
|
60
|
+
};
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Formatter Plugin
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
matches: (field, fieldTag, fieldType) => {
|
|
67
|
+
return fieldType === FieldType.Number && fieldTag.has('currency');
|
|
68
|
+
};
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Conditional Styling
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
renderComponent: (props) => {
|
|
75
|
+
const value = props.dataColumn.value;
|
|
76
|
+
const className = value > 100 ? 'high' : 'low';
|
|
77
|
+
return <div class={className}>{value}</div>;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Usage in Malloy
|
|
82
|
+
|
|
83
|
+
```malloy
|
|
84
|
+
source: data is table('data.csv') {
|
|
85
|
+
group_by:
|
|
86
|
+
# currency
|
|
87
|
+
price
|
|
88
|
+
# my_chart
|
|
89
|
+
sales_trend
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Registration
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
const renderer = new MalloyRenderer({
|
|
97
|
+
plugins: [MinimalPluginFactory],
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Next steps
|
|
102
|
+
|
|
103
|
+
Once your plugin reads tags beyond just the one that triggers `matches()`, read [validation.md](validation.md). It covers when to throw (red error tile), when to log (source-located validation error), and how to declare tag ownership so valid tag uses don't trigger unread-tag warnings.
|