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.
@@ -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 typeDir = this.side == 'bottom' ? 'row' : 'column'
43
- container.div(`.tt-flex.wrapped.small-gap.${typeDir}`, flex => {
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 {DivePlotTrace} from "./dive-plot-trace"
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("hub-plot")
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", "hub-plus", "secondary")
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
@@ -4,7 +4,7 @@
4
4
  "files": [
5
5
  "*"
6
6
  ],
7
- "version": "4.32.1",
7
+ "version": "4.32.2",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "https://github.com/Terrier-Tech/terrier-engine"
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
- }