terrier-engine 4.32.1 → 4.32.2
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/data-dive/plots/dive-plot-axes.ts +7 -13
- package/data-dive/plots/dive-plot-editor.ts +70 -4
- package/data-dive/plots/dive-plot-list.ts +1 -1
- package/data-dive/plots/dive-plot-traces.ts +233 -0
- package/package.json +1 -1
- package/terrier/forms.ts +15 -0
- package/terrier/theme.ts +4 -0
- package/data-dive/plots/dive-plot-trace.ts +0 -16
|
@@ -18,13 +18,17 @@ export type DivePlotAxis = {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const axisTypeOptions = [
|
|
21
|
-
{value: 'none', title: 'None'},
|
|
22
21
|
{value: 'number', title: 'Number'},
|
|
23
22
|
{value: 'time', title: 'Date/Time'},
|
|
24
23
|
{value: 'group', title: 'Grouped Bars'},
|
|
25
24
|
{value: 'stack', title: 'Stacked Bars'},
|
|
26
25
|
]
|
|
27
26
|
|
|
27
|
+
// the right axis isn't required
|
|
28
|
+
const rightAxisTypeOptions = axisTypeOptions.concat([
|
|
29
|
+
{value: 'none', title: 'None'}
|
|
30
|
+
])
|
|
31
|
+
|
|
28
32
|
export class DivePlotAxisFields extends TerrierFormFields<DivePlotAxis> {
|
|
29
33
|
|
|
30
34
|
constructor(part: TerrierPart<any>, axis: DivePlotAxis, readonly side: AxisSide, errors?: DbErrors<DivePlotAxis>) {
|
|
@@ -39,18 +43,8 @@ export class DivePlotAxisFields extends TerrierFormFields<DivePlotAxis> {
|
|
|
39
43
|
this.textInput(container, "title", {placeholder: "Title"})
|
|
40
44
|
|
|
41
45
|
// type
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
axisTypeOptions.forEach(option => {
|
|
45
|
-
if ((this.side == 'bottom' || this.side == 'left') && option.value == 'none') {
|
|
46
|
-
return // don't let them not have a bottom or left axis
|
|
47
|
-
}
|
|
48
|
-
flex.label(".caption-size", label => {
|
|
49
|
-
this.radio(label, 'type', option.value)
|
|
50
|
-
label.span().text(option.title)
|
|
51
|
-
})
|
|
52
|
-
})
|
|
53
|
-
})
|
|
46
|
+
const typeOptions = this.side == 'right' ? rightAxisTypeOptions : axisTypeOptions
|
|
47
|
+
this.select(container, 'type', typeOptions)
|
|
54
48
|
})
|
|
55
49
|
}
|
|
56
50
|
}
|
|
@@ -9,7 +9,13 @@ import DivePlotList from "./dive-plot-list"
|
|
|
9
9
|
import Db from "../dd-db"
|
|
10
10
|
import DivePlotRenderPart from "./dive-plot-render-part"
|
|
11
11
|
import {DivePlotAxis, DivePlotAxisFields} from "./dive-plot-axes"
|
|
12
|
-
import {
|
|
12
|
+
import DivePlotTraces, {
|
|
13
|
+
DivePlotTrace,
|
|
14
|
+
DivePlotTraceEditor,
|
|
15
|
+
DivePlotTraceRow,
|
|
16
|
+
} from "./dive-plot-traces"
|
|
17
|
+
import Fragments from "../../terrier/fragments"
|
|
18
|
+
import Arrays from "tuff-core/arrays"
|
|
13
19
|
|
|
14
20
|
const log = new Logger("DivePlotList")
|
|
15
21
|
|
|
@@ -21,10 +27,14 @@ export default class DivePlotEditor extends ModalPart<DivePlotEditorState> {
|
|
|
21
27
|
|
|
22
28
|
plot!: UnpersistedDdDivePlot
|
|
23
29
|
fields!: TerrierFormFields<UnpersistedDdDivePlot>
|
|
30
|
+
|
|
24
31
|
leftAxisFields!: DivePlotAxisFields
|
|
25
32
|
rightAxisFields!: DivePlotAxisFields
|
|
26
33
|
bottomAxisFields!: DivePlotAxisFields
|
|
34
|
+
|
|
35
|
+
newTraceKey = Messages.untypedKey()
|
|
27
36
|
traces: DivePlotTrace[] = []
|
|
37
|
+
|
|
28
38
|
renderPart!: DivePlotRenderPart
|
|
29
39
|
saveKey = Messages.untypedKey()
|
|
30
40
|
|
|
@@ -37,7 +47,7 @@ export default class DivePlotEditor extends ModalPart<DivePlotEditorState> {
|
|
|
37
47
|
else {
|
|
38
48
|
this.setTitle("New Dive Plot")
|
|
39
49
|
}
|
|
40
|
-
this.setIcon("
|
|
50
|
+
this.setIcon("glyp-differential")
|
|
41
51
|
|
|
42
52
|
this.fields = new TerrierFormFields<UnpersistedDdDivePlot>(this, this.plot)
|
|
43
53
|
|
|
@@ -50,7 +60,35 @@ export default class DivePlotEditor extends ModalPart<DivePlotEditorState> {
|
|
|
50
60
|
const bottomAxis: DivePlotAxis = axes['bottom'] || {type: 'number', title: ''}
|
|
51
61
|
this.bottomAxisFields = new DivePlotAxisFields(this, bottomAxis, 'bottom')
|
|
52
62
|
|
|
63
|
+
// trace editors
|
|
53
64
|
this.traces = this.plot.traces || []
|
|
65
|
+
this.updateTraces()
|
|
66
|
+
|
|
67
|
+
this.onClick(this.newTraceKey, _ => {
|
|
68
|
+
log.info("Showing new trace form")
|
|
69
|
+
const state = {
|
|
70
|
+
...this.state,
|
|
71
|
+
trace: DivePlotTraces.blankTrace(),
|
|
72
|
+
onSave: (newTrace: DivePlotTrace) => this.addTrace(newTrace),
|
|
73
|
+
onDelete: (_: DivePlotTrace) => {}
|
|
74
|
+
}
|
|
75
|
+
this.app.showModal(DivePlotTraceEditor, state)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
this.onClick(DivePlotTraces.editKey, m => {
|
|
79
|
+
const id = m.data.id
|
|
80
|
+
log.info(`Editing plot trace ${id}`)
|
|
81
|
+
const trace = this.traces.find(t => t.id==id)
|
|
82
|
+
if (trace) {
|
|
83
|
+
const state = {
|
|
84
|
+
...this.state,
|
|
85
|
+
trace,
|
|
86
|
+
onSave: (newTrace: DivePlotTrace) => this.replaceTrace(newTrace),
|
|
87
|
+
onDelete: (trace: DivePlotTrace) => this.removeTrace(trace),
|
|
88
|
+
}
|
|
89
|
+
this.app.showModal(DivePlotTraceEditor, state)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
54
92
|
|
|
55
93
|
this.renderPart = this.makePart(DivePlotRenderPart, this.state)
|
|
56
94
|
|
|
@@ -66,8 +104,31 @@ export default class DivePlotEditor extends ModalPart<DivePlotEditorState> {
|
|
|
66
104
|
})
|
|
67
105
|
}
|
|
68
106
|
|
|
107
|
+
addTrace(trace: DivePlotTrace) {
|
|
108
|
+
this.traces.push(trace)
|
|
109
|
+
this.updateTraces()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
replaceTrace(trace: DivePlotTrace) {
|
|
113
|
+
// TODO: implement this in tuff-core
|
|
114
|
+
log.info(`Replacing trace ${trace.id}`, trace)
|
|
115
|
+
this.traces = this.traces.map(t => t.id === trace.id ? trace : t)
|
|
116
|
+
this.updateTraces()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
removeTrace(trace: DivePlotTrace) {
|
|
120
|
+
log.info(`Removing trace ${trace.id}`, trace)
|
|
121
|
+
this.traces = Arrays.compact(this.traces.map(t => t.id === trace.id ? null : t))
|
|
122
|
+
this.updateTraces()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
updateTraces() {
|
|
126
|
+
this.assignCollection('traces', DivePlotTraceRow, this.traces || [])
|
|
127
|
+
this.dirty()
|
|
128
|
+
}
|
|
129
|
+
|
|
69
130
|
renderContent(parent: PartTag): void {
|
|
70
|
-
parent.div(".tt-flex.column.padded.gap", mainColumn => {
|
|
131
|
+
parent.div(".tt-flex.column.padded.large-gap", mainColumn => {
|
|
71
132
|
|
|
72
133
|
mainColumn.div(".dd-plot-axes-and-preview.tt-flex.column.gap", axesAndPreview => {
|
|
73
134
|
this.fields.compoundField(axesAndPreview, field => {
|
|
@@ -85,12 +146,17 @@ export default class DivePlotEditor extends ModalPart<DivePlotEditorState> {
|
|
|
85
146
|
|
|
86
147
|
mainColumn.h3(".glyp-items").text("Traces")
|
|
87
148
|
|
|
149
|
+
this.renderCollection(mainColumn, 'traces')
|
|
150
|
+
|
|
151
|
+
mainColumn.div('.tt-flex.justify-center', row => {
|
|
152
|
+
Fragments.button(row, this.theme, "New Trace", 'glyp-plus')
|
|
153
|
+
}).emitClick(this.newTraceKey)
|
|
88
154
|
})
|
|
89
155
|
}
|
|
90
156
|
|
|
91
157
|
async save() {
|
|
92
158
|
const plotData = await this.fields.serialize()
|
|
93
|
-
const plot = {...this.plot, title: plotData.title}
|
|
159
|
+
const plot = {...this.plot, title: plotData.title, traces: this.traces}
|
|
94
160
|
|
|
95
161
|
plot.layout.axes = {
|
|
96
162
|
left: await this.leftAxisFields.serialize(),
|
|
@@ -100,7 +100,7 @@ export default class DivePlotList extends TerrierPart<DiveEditorState> {
|
|
|
100
100
|
render(parent: PartTag): any {
|
|
101
101
|
this.renderCollection(parent, "plots")
|
|
102
102
|
|
|
103
|
-
Fragments.button(parent, this.theme, "New Plot", "
|
|
103
|
+
Fragments.button(parent, this.theme, "New Plot", "glyp-plus_outline", "secondary")
|
|
104
104
|
.emitClick(this.newKey)
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import {MarkerStyle, TraceStyle, TraceType, YAxisName} from "tuff-plot/trace"
|
|
2
|
+
import TerrierFormPart from "../../terrier/parts/terrier-form-part"
|
|
3
|
+
import { PartTag } from "tuff-core/parts"
|
|
4
|
+
import {ModalPart} from "../../terrier/modals"
|
|
5
|
+
import Ids from "../../terrier/ids"
|
|
6
|
+
import {DivePlotEditorState} from "./dive-plot-editor"
|
|
7
|
+
import {TerrierFormFields} from "../../terrier/forms"
|
|
8
|
+
import {UnpersistedDdDivePlot} from "../gen/models"
|
|
9
|
+
import {SelectOptions} from "tuff-core/forms"
|
|
10
|
+
import Queries, {Query} from "../queries/queries"
|
|
11
|
+
import {Logger} from "tuff-core/logging"
|
|
12
|
+
import Columns from "../queries/columns";
|
|
13
|
+
import Messages from "tuff-core/messages";
|
|
14
|
+
|
|
15
|
+
const log = new Logger("DivePlotTraces")
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Similar to the tuff-plot Trace but not strongly typed to the data type since it's dynamically assigned to a query.
|
|
19
|
+
*/
|
|
20
|
+
export type DivePlotTrace = {
|
|
21
|
+
id: string
|
|
22
|
+
type: TraceType
|
|
23
|
+
title: string
|
|
24
|
+
query_id: string
|
|
25
|
+
x: string
|
|
26
|
+
y: string
|
|
27
|
+
y_axis: YAxisName
|
|
28
|
+
style?: TraceStyle
|
|
29
|
+
marker?: MarkerStyle
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a new blank trace.
|
|
34
|
+
*/
|
|
35
|
+
function blankTrace(): DivePlotTrace {
|
|
36
|
+
return {
|
|
37
|
+
id: Ids.makeUuid(),
|
|
38
|
+
type: 'scatter',
|
|
39
|
+
title: '',
|
|
40
|
+
query_id: '',
|
|
41
|
+
x: '',
|
|
42
|
+
y: '',
|
|
43
|
+
y_axis: 'left',
|
|
44
|
+
style: {}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
/// Trace Style Fields
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Form fields for editing trace style.
|
|
53
|
+
*/
|
|
54
|
+
class TraceStyleFields extends TerrierFormFields<TraceStyle> {
|
|
55
|
+
|
|
56
|
+
render(parent: PartTag) {
|
|
57
|
+
parent.div('.tt-form.tt-flex.gap.wrap', container => {
|
|
58
|
+
this.compoundField(container, field => {
|
|
59
|
+
field.label().text("Color")
|
|
60
|
+
this.textInput(field, 'stroke', {placeholder: 'Color'})
|
|
61
|
+
})
|
|
62
|
+
this.compoundField(container, field => {
|
|
63
|
+
field.label().text("Width")
|
|
64
|
+
this.numberInput(field, 'strokeWidth', {placeholder: 'Width'})
|
|
65
|
+
})
|
|
66
|
+
this.compoundField(container, field => {
|
|
67
|
+
field.label().text("Dashes")
|
|
68
|
+
this.textInput(field, 'strokeDasharray', {placeholder: 'Dashes'})
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
/// Editor
|
|
78
|
+
|
|
79
|
+
export type DivePlotTraceState = DivePlotEditorState & {
|
|
80
|
+
trace: DivePlotTrace
|
|
81
|
+
onSave: (trace: DivePlotTrace) => any
|
|
82
|
+
onDelete: (trace: DivePlotTrace) => any
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const editKey = Messages.typedKey<{ id: string }>()
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Editor for a single plot trace.
|
|
89
|
+
*/
|
|
90
|
+
export class DivePlotTraceEditor extends ModalPart<DivePlotTraceState> {
|
|
91
|
+
|
|
92
|
+
fields!: TerrierFormFields<DivePlotTrace>
|
|
93
|
+
queries: Query[] = []
|
|
94
|
+
plot!: UnpersistedDdDivePlot
|
|
95
|
+
trace!: DivePlotTrace
|
|
96
|
+
|
|
97
|
+
queryOptions!: SelectOptions
|
|
98
|
+
axisOptions: string[] = []
|
|
99
|
+
|
|
100
|
+
styleFields!: TraceStyleFields
|
|
101
|
+
|
|
102
|
+
saveKey = Messages.untypedKey()
|
|
103
|
+
deleteKey = Messages.untypedKey()
|
|
104
|
+
|
|
105
|
+
async init() {
|
|
106
|
+
this.setTitle("Plot Trace Editor")
|
|
107
|
+
this.setIcon("glyp-items")
|
|
108
|
+
|
|
109
|
+
this.plot = this.state.plot
|
|
110
|
+
this.trace = this.state.trace
|
|
111
|
+
|
|
112
|
+
this.queries = this.state.dive.query_data?.queries || []
|
|
113
|
+
this.queryOptions = this.queries.map(query => {
|
|
114
|
+
return {value: query.id, title: query.name}
|
|
115
|
+
}) || []
|
|
116
|
+
|
|
117
|
+
this.trace.query_id ||= this.queries.at(0)?.id || ''
|
|
118
|
+
this.updateAxisOptions(this.trace.query_id)
|
|
119
|
+
|
|
120
|
+
this.fields = new TerrierFormFields<DivePlotTrace>(this, this.state.trace)
|
|
121
|
+
|
|
122
|
+
this.styleFields = new TraceStyleFields(this, this.trace.style || {})
|
|
123
|
+
|
|
124
|
+
this.addAction({
|
|
125
|
+
title: "Save",
|
|
126
|
+
icon: "glyp-checkmark",
|
|
127
|
+
click: {key: this.saveKey}
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
this.addAction({
|
|
131
|
+
title: "Delete",
|
|
132
|
+
icon: "glyp-delete",
|
|
133
|
+
click: {key: this.deleteKey},
|
|
134
|
+
classes: ['alert']
|
|
135
|
+
}, 'secondary')
|
|
136
|
+
|
|
137
|
+
this.onClick(this.saveKey, _ => {
|
|
138
|
+
this.save()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
this.onClick(this.deleteKey, _ => {
|
|
142
|
+
this.state.onDelete(this.trace)
|
|
143
|
+
this.pop()
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Update the axis options based on the query.
|
|
149
|
+
*/
|
|
150
|
+
updateAxisOptions(queryId: string) {
|
|
151
|
+
const query = this.queries.find(q => q.id == queryId)
|
|
152
|
+
this.axisOptions = []
|
|
153
|
+
if (query) {
|
|
154
|
+
log.info(`Computing axis options for query`, query)
|
|
155
|
+
Queries.eachColumn(query, (table, column) => {
|
|
156
|
+
this.axisOptions.push(Columns.computeSelectName(table, column))
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
log.warn(`No query with id ${queryId}`)
|
|
161
|
+
}
|
|
162
|
+
this.dirty()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
renderContent(parent: PartTag): void {
|
|
166
|
+
parent.div(".tt-form.tt-flex.large-gap.column.padded", mainColumn => {
|
|
167
|
+
// query
|
|
168
|
+
this.fields.compoundField(mainColumn, field => {
|
|
169
|
+
field.label().text("Query")
|
|
170
|
+
this.fields.select(field, 'query_id', this.queryOptions)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// axes
|
|
174
|
+
mainColumn.div('.tt-flex.gap', row => {
|
|
175
|
+
row.div('.tt-flex.column.gap', col => {
|
|
176
|
+
col.h3().text("X Column")
|
|
177
|
+
for (const c of this.axisOptions) {
|
|
178
|
+
this.fields.radioLabel(col, 'x', c, c)
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
row.div('.tt-flex.column.gap', col => {
|
|
182
|
+
col.h3().text("Y Column")
|
|
183
|
+
for (const c of this.axisOptions) {
|
|
184
|
+
this.fields.radioLabel(col, 'y', c, c)
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// style
|
|
190
|
+
mainColumn.div('.tt-flex.gap.column', styleRow => {
|
|
191
|
+
styleRow.h3().text("Style")
|
|
192
|
+
this.styleFields.render(styleRow)
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async save() {
|
|
198
|
+
const data = await this.fields.serialize()
|
|
199
|
+
this.trace = {...this.trace, ...data}
|
|
200
|
+
log.info("Saving plot trace", this.trace)
|
|
201
|
+
this.state.onSave(this.trace)
|
|
202
|
+
this.pop()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
/// Row
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Row for displaying a single plot trace.
|
|
212
|
+
*/
|
|
213
|
+
export class DivePlotTraceRow extends TerrierFormPart<DivePlotTrace> {
|
|
214
|
+
|
|
215
|
+
render(parent: PartTag) {
|
|
216
|
+
const trace = this.state
|
|
217
|
+
parent.a('.dd-dive-plot-trace-row.tt-panel.padded', panel => {
|
|
218
|
+
panel.div('.panel-content.tt-flex.row.gap', content => {
|
|
219
|
+
content.div('.axes').text(`${trace.x} -> ${trace.y}`)
|
|
220
|
+
})
|
|
221
|
+
}).emitClick(editKey, {id: trace.id})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
/// Export
|
|
228
|
+
|
|
229
|
+
const DivePlotTraces = {
|
|
230
|
+
blankTrace,
|
|
231
|
+
editKey
|
|
232
|
+
}
|
|
233
|
+
export default DivePlotTraces
|
package/package.json
CHANGED
package/terrier/forms.ts
CHANGED
|
@@ -155,6 +155,21 @@ export class TerrierFormFields<T extends FormPartData> extends FormFields<T> {
|
|
|
155
155
|
})
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Makes a label with a radio and title span inside of it.
|
|
160
|
+
* @param parent
|
|
161
|
+
* @param name
|
|
162
|
+
* @param value
|
|
163
|
+
* @param title the title to put in the label span
|
|
164
|
+
* @param attrs attributes for the radio input
|
|
165
|
+
*/
|
|
166
|
+
radioLabel<Key extends KeyOfType<T, string> & string>(parent: PartTag, name: Key, value: string, title?: string, attrs: InputTagAttrs = {}) {
|
|
167
|
+
return parent.label('.body-size', label => {
|
|
168
|
+
this.radio(label, name, value, attrs)
|
|
169
|
+
label.span().text(title || value)
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
158
173
|
}
|
|
159
174
|
|
|
160
175
|
|
package/terrier/theme.ts
CHANGED
|
@@ -35,6 +35,7 @@ export type Action = {
|
|
|
35
35
|
subtitle?: string
|
|
36
36
|
tooltip?: string
|
|
37
37
|
icon?: IconName
|
|
38
|
+
img?: string
|
|
38
39
|
href?: string
|
|
39
40
|
classes?: string[]
|
|
40
41
|
click?: Packet
|
|
@@ -101,6 +102,9 @@ export default class Theme {
|
|
|
101
102
|
if (action.icon?.length) {
|
|
102
103
|
this.renderIcon(a, action.icon, iconColor)
|
|
103
104
|
}
|
|
105
|
+
if (action.img?.length) {
|
|
106
|
+
a.img('.image', {src: action.img})
|
|
107
|
+
}
|
|
104
108
|
if (action.title?.length) {
|
|
105
109
|
a.div('.title', {text: action.title})
|
|
106
110
|
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import {MarkerStyle, TraceStyle, TraceType, YAxisName} from "tuff-plot/trace";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Similar to the tuff-plot Trace but not strongly typed to the data type since it's dynamically assigned to a query.
|
|
5
|
-
*/
|
|
6
|
-
export type DivePlotTrace = {
|
|
7
|
-
id: string
|
|
8
|
-
type: TraceType
|
|
9
|
-
title: string
|
|
10
|
-
query_id: string
|
|
11
|
-
x: string
|
|
12
|
-
y: string
|
|
13
|
-
y_axis: YAxisName
|
|
14
|
-
style?: TraceStyle
|
|
15
|
-
marker?: MarkerStyle
|
|
16
|
-
}
|