@operato/scene-legend 0.0.11

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.
Files changed (50) hide show
  1. package/@types/global/index.d.ts +1 -0
  2. package/CHANGELOG.md +12 -0
  3. package/LICENSE +21 -0
  4. package/README.md +31 -0
  5. package/assets/legend.png +0 -0
  6. package/dist/editors/editor-legend-status.d.ts +4 -0
  7. package/dist/editors/editor-legend-status.js +313 -0
  8. package/dist/editors/editor-legend-status.js.map +1 -0
  9. package/dist/editors/index.d.ts +5 -0
  10. package/dist/editors/index.js +11 -0
  11. package/dist/editors/index.js.map +1 -0
  12. package/dist/editors/property-editor-legend-status.d.ts +7 -0
  13. package/dist/editors/property-editor-legend-status.js +13 -0
  14. package/dist/editors/property-editor-legend-status.js.map +1 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js +6 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/legend-item.d.ts +15 -0
  19. package/dist/legend-item.js +42 -0
  20. package/dist/legend-item.js.map +1 -0
  21. package/dist/legend.d.ts +40 -0
  22. package/dist/legend.js +177 -0
  23. package/dist/legend.js.map +1 -0
  24. package/dist/templates/index.d.ts +18 -0
  25. package/dist/templates/index.js +3 -0
  26. package/dist/templates/index.js.map +1 -0
  27. package/dist/templates/legend.d.ts +18 -0
  28. package/dist/templates/legend.js +19 -0
  29. package/dist/templates/legend.js.map +1 -0
  30. package/helps/scene/component/legend.ko.md +18 -0
  31. package/helps/scene/component/legend.md +18 -0
  32. package/helps/scene/component/legend.zh.md +20 -0
  33. package/helps/scene/images/legend-01.png +0 -0
  34. package/helps/scene/images/legend-02.png +0 -0
  35. package/package.json +63 -0
  36. package/src/editors/editor-legend-status.ts +339 -0
  37. package/src/editors/index.ts +11 -0
  38. package/src/editors/property-editor-legend-status.ts +17 -0
  39. package/src/index.ts +6 -0
  40. package/src/legend-item.ts +52 -0
  41. package/src/legend.ts +232 -0
  42. package/src/templates/index.ts +3 -0
  43. package/src/templates/legend.ts +19 -0
  44. package/test/basic-test.html +67 -0
  45. package/test/index.html +24 -0
  46. package/test/unit/test-legend.js +33 -0
  47. package/test/unit/util.js +22 -0
  48. package/things-scene.config.js +7 -0
  49. package/tsconfig.json +23 -0
  50. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,339 @@
1
+ /**
2
+ * @license Copyright © HatioLab Inc. All rights reserved.
3
+ */
4
+
5
+ import { LitElement, PropertyValues, css, html } from 'lit'
6
+ import { customElement, property, state } from 'lit/decorators.js'
7
+
8
+ @customElement('editor-legend-status')
9
+ class EditorLegendStatus extends LitElement {
10
+ static styles = [
11
+ css`
12
+ :host {
13
+ font-size: 0.8em;
14
+ display: grid;
15
+ grid-template-columns: repeat(10, 1fr);
16
+ grid-gap: 5px;
17
+ }
18
+
19
+ :host > * {
20
+ order: 2;
21
+ grid-column: 4 / -1;
22
+ }
23
+
24
+ :host > legend {
25
+ order: 1;
26
+ grid-column: 1 / -1;
27
+ font-size: 11px;
28
+ color: rgb(228, 108, 46);
29
+ font-weight: bold;
30
+ text-transform: capitalize;
31
+ padding: 5px 0px 0px 5px;
32
+ }
33
+
34
+ :host > label {
35
+ grid-column: 1 / 4;
36
+ text-align: right;
37
+ color: var(--primary-text-color);
38
+ }
39
+
40
+ div[data-record] input {
41
+ width: 20%;
42
+ }
43
+ :host > table {
44
+ grid-column: 1 / -1;
45
+ }
46
+ table input {
47
+ width: 25px;
48
+ margin: 3px 0 2px 0;
49
+ padding: 3px;
50
+ font-size: 12px;
51
+ }
52
+ table td span {
53
+ padding: 5px 0 0 0;
54
+ }
55
+ table td things-editor-color {
56
+ width: 81px;
57
+ height: 25px;
58
+ }
59
+ table td button {
60
+ margin-left: 0;
61
+ }
62
+ table th {
63
+ background-color: rgba(0, 0, 0, 0.1);
64
+ padding: 2px 0;
65
+ text-align: center;
66
+ }
67
+
68
+ table tr > th:first-child {
69
+ width: 40px;
70
+ }
71
+
72
+ table tr > th:nth-child(2) {
73
+ width: 85px;
74
+ }
75
+
76
+ table tr > th:nth-child(4) {
77
+ width: 30px;
78
+ }
79
+
80
+ table *.things-editor-legend-status {
81
+ float: none !important;
82
+ }
83
+ table td {
84
+ text-align: center;
85
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
86
+ }
87
+ table tr.stock-new {
88
+ background-color: rgba(179, 145, 117, 0.3);
89
+ }
90
+ table td input[data-description] {
91
+ width: 100%;
92
+ box-sizing: border-box;
93
+ }
94
+ `
95
+ ]
96
+
97
+ @property({ type: Object }) value: any
98
+
99
+ @state() private _statusField?: string
100
+ @state() private _defaultColor?: string
101
+ @state() private _ranges: any[] = []
102
+
103
+ private boundOnChange?: any
104
+ private _changingNow: boolean = false
105
+
106
+ render() {
107
+ return html`
108
+ <legend>
109
+ <i18n-msg msgid="label.status">Status</i18n-msg>
110
+ </legend>
111
+
112
+ <label class="stock-field">
113
+ <i18n-msg msgid="label.field">Field</i18n-msg>
114
+ </label>
115
+ <input
116
+ type="text"
117
+ .value=${this._statusField || ''}
118
+ @change=${(e: Event) => {
119
+ this._statusField = (e.target as HTMLInputElement).value
120
+ }}
121
+ />
122
+ <label class="default-color">
123
+ <i18n-msg msgid="label.default-color">Default Color</i18n-msg>
124
+ </label>
125
+ <things-editor-color
126
+ name="default-color"
127
+ .value=${this._defaultColor || ''}
128
+ placeholder="default color"
129
+ @change=${(e: Event) => {
130
+ this._defaultColor = (e.target as HTMLInputElement).value
131
+ }}
132
+ ></things-editor-color>
133
+
134
+ <table>
135
+ <tr>
136
+ <th>
137
+ Min &le; <br />Field<br />
138
+ &lt; Max
139
+ </th>
140
+ <th>color</th>
141
+ <th>disp. text</th>
142
+ <th></th>
143
+ </tr>
144
+ ${this._ranges.map(
145
+ item => html`
146
+ <tr data-record>
147
+ <td>
148
+ <input type="text" data-min placeholder="min" .value="${item.min}" />
149
+ <span>~</span>
150
+ <input type="text" data-max placeholder="max" .value="${item.max}" />
151
+ </td>
152
+ <td>
153
+ <things-editor-color data-color .value="${item.color}" placeholder="color"></things-editor-color>
154
+ </td>
155
+ <td>
156
+ <input type="text" data-description .value="${item.description || ''}" placeholder="display text" />
157
+ </td>
158
+ <td>
159
+ <button class="record-action" @tap=${(e: TouchEvent) => this._delete(e)} tabindex="-1">-</button>
160
+ </td>
161
+ </tr>
162
+ `
163
+ )}
164
+
165
+ <tr data-record-new class="stock-new">
166
+ <td>
167
+ <input type="text" data-min placeholder="min" value="" />
168
+ <span>~</span>
169
+ <input type="text" data-max placeholder="max" value="" />
170
+ </td>
171
+ <td>
172
+ <things-editor-color data-color value="" placeholder="color"></things-editor-color>
173
+ </td>
174
+ <td>
175
+ <input type="text" data-description value="" placeholder="display text" />
176
+ </td>
177
+ <td>
178
+ <button class="record-action" @tap=${() => this._add()} tabindex="-1">+</button>
179
+ </td>
180
+ </tr>
181
+ </table>
182
+ `
183
+ }
184
+
185
+ connectedCallback() {
186
+ super.connectedCallback()
187
+ if (!this.boundOnChange) this.boundOnChange = this._onChange.bind(this)
188
+
189
+ this.renderRoot.addEventListener('change', this.boundOnChange)
190
+ }
191
+
192
+ disconnectedCallback() {
193
+ super.disconnectedCallback()
194
+ this.renderRoot.removeEventListener('change', this.boundOnChange)
195
+ }
196
+
197
+ _valueChanged(value: any) {
198
+ var val = value || this._getDefaultValue()
199
+ this._statusField = val.field
200
+ this._defaultColor = val.defaultColor
201
+ this._ranges = [...val.ranges]
202
+
203
+ this.requestUpdate()
204
+ }
205
+
206
+ _onChange(e: Event) {
207
+ e.stopPropagation()
208
+ this._changingNow = true
209
+
210
+ var input = e.target as HTMLInputElement
211
+ var value = input.value
212
+
213
+ var tr = input.closest('tr')
214
+
215
+ if (tr) {
216
+ if (tr.hasAttribute('data-record')) this._build(true)
217
+ else if (tr.hasAttribute('data-record-new') && input.hasAttribute('data-color')) this._add()
218
+ }
219
+
220
+ this.value = {
221
+ field: this._statusField,
222
+ defaultColor: this._defaultColor,
223
+ ranges: this._ranges
224
+ }
225
+
226
+ this.dispatchEvent(
227
+ new CustomEvent('change', {
228
+ bubbles: true,
229
+ composed: true
230
+ })
231
+ )
232
+ this.requestUpdate()
233
+ }
234
+
235
+ _build(includeNewRecord: boolean) {
236
+ if (includeNewRecord) var records = this.renderRoot.querySelectorAll('[data-record],[data-record-new]')
237
+ else var records = this.renderRoot.querySelectorAll('[data-record]')
238
+
239
+ var newRanges = []
240
+
241
+ for (var i = 0; i < records.length; i++) {
242
+ var record = records[i]
243
+
244
+ var min = (record.querySelector('[data-min]') as HTMLInputElement).value
245
+ var max = (record.querySelector('[data-max]') as HTMLInputElement).value
246
+ var description = (record.querySelector('[data-description]') as HTMLInputElement).value
247
+ var inputs = record.querySelectorAll('[data-color]:not([style*="display: none"])') as NodeListOf<HTMLInputElement>
248
+ if (!inputs || inputs.length == 0) continue
249
+
250
+ var input = inputs[inputs.length - 1]
251
+ var color = input.value
252
+
253
+ if (min != undefined && max != undefined && color)
254
+ newRanges.push({
255
+ min: min.trim(),
256
+ max: max.trim(),
257
+ color: color.trim(),
258
+ description: description.trim()
259
+ })
260
+ }
261
+
262
+ newRanges.sort(function (range1, range2) {
263
+ var min1 = Number(range1.min)
264
+ var min2 = Number(range2.min)
265
+
266
+ var result = min1 - min2
267
+
268
+ if (Number.isNaN(result)) {
269
+ var strMin1 = String(min1)
270
+ var strMin2 = String(min2)
271
+
272
+ if (strMin1 > strMin2) result = 1
273
+ else if (strMin1 == strMin2) result = 0
274
+ else result = -1
275
+ }
276
+
277
+ return result
278
+ })
279
+
280
+ this._ranges = newRanges
281
+ this.requestUpdate()
282
+ }
283
+
284
+ _add() {
285
+ this._build(true)
286
+
287
+ var inputs = this.renderRoot.querySelectorAll(
288
+ '[data-record-new] input:not([style*="display: none"]), [data-record-new] [data-color]:not([style*="display: none"])'
289
+ ) as NodeListOf<HTMLInputElement>
290
+
291
+ for (var i = 0; i < inputs.length; i++) {
292
+ let input = inputs[i]
293
+ input.value = ''
294
+ }
295
+ }
296
+
297
+ _delete(e: Event) {
298
+ var record = (e.target as Element).closest('tr[data-record]')
299
+
300
+ ;(record!.querySelector('[data-min]') as HTMLInputElement).value = ''
301
+ ;(record!.querySelector('[data-max]') as HTMLInputElement).value = ''
302
+ ;(record!.querySelector('[data-color]') as HTMLInputElement).value = ''
303
+
304
+ this._build(false)
305
+
306
+ this.value = {
307
+ field: this._statusField,
308
+ defaultColor: this._defaultColor,
309
+ ranges: this._ranges
310
+ }
311
+
312
+ this.dispatchEvent(
313
+ new CustomEvent('change', {
314
+ bubbles: true,
315
+ composed: true
316
+ })
317
+ )
318
+ }
319
+
320
+ _getDefaultValue() {
321
+ return {
322
+ field: '',
323
+ defaultColor: '',
324
+ ranges: []
325
+ }
326
+ }
327
+
328
+ _onRepeaterChanged() {
329
+ var inputs = this.renderRoot.querySelectorAll(
330
+ '[data-record] input:not([style*="display: none"])[value=""], [data-record-new] input:not([style*="display: none"])[value=""]'
331
+ ) as NodeListOf<HTMLInputElement>
332
+
333
+ inputs[0].focus()
334
+ }
335
+
336
+ updated(changes: PropertyValues<this>) {
337
+ if (changes.has('value')) this._valueChanged(this.value)
338
+ }
339
+ }
@@ -0,0 +1,11 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ */
4
+ import { PropertyEditorLegendStatus } from './property-editor-legend-status'
5
+
6
+ export default [
7
+ {
8
+ type: 'legend-status',
9
+ element: PropertyEditorLegendStatus.is
10
+ }
11
+ ]
@@ -0,0 +1,17 @@
1
+ import './editor-legend-status'
2
+
3
+ import { OxPropertyEditor } from '@operato/property-editor'
4
+ import { Properties } from '@hatiolab/things-scene'
5
+ import { html } from 'lit-element'
6
+
7
+ export class PropertyEditorLegendStatus extends OxPropertyEditor {
8
+ static get is() {
9
+ return 'property-editor-legend-status'
10
+ }
11
+
12
+ editorTemplate(props: Properties) {
13
+ return html` <editor-legend-status .value=${props.value} fullwidth></editor-legend-status> `
14
+ }
15
+ }
16
+
17
+ customElements.define(PropertyEditorLegendStatus.is, PropertyEditorLegendStatus)
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ */
4
+
5
+ export { default as Legend } from './legend'
6
+ export { default as LegendItem } from './legend-item'
@@ -0,0 +1,52 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ */
4
+ import { Component, Model, Properties, RectPath, Shape } from '@hatiolab/things-scene'
5
+
6
+ const NATURE = {
7
+ mutable: false,
8
+ resizable: false,
9
+ rotatable: false,
10
+ properties: []
11
+ }
12
+
13
+ export default class LegendItem extends RectPath(Shape) {
14
+ render(context: CanvasRenderingContext2D) {
15
+ var { left, top, height, color } = this.model
16
+
17
+ context.beginPath()
18
+
19
+ var c = height / 2
20
+ var r = c / 2
21
+
22
+ context.save()
23
+
24
+ context.fillStyle = color
25
+ context.ellipse(left + c, top + c, r, r, 0, 0, Math.PI * 2, true)
26
+ context.shadowColor = 'rgba(0,0,0,0.15)'
27
+ context.shadowBlur = 2
28
+ context.shadowOffsetX = 1
29
+ context.shadowOffsetY = 2
30
+ context.fill()
31
+
32
+ context.restore()
33
+ }
34
+
35
+ onchange(after: Properties) {
36
+ if (after.hasOwnProperty('height')) this.set('paddingLeft', after.height)
37
+ }
38
+
39
+ get stuck() {
40
+ return true
41
+ }
42
+
43
+ get capturable() {
44
+ return false
45
+ }
46
+
47
+ get nature() {
48
+ return NATURE
49
+ }
50
+ }
51
+
52
+ Component.register('legend-item', LegendItem)
package/src/legend.ts ADDED
@@ -0,0 +1,232 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ */
4
+ import { Component, Container, Model, POSITION, Properties, TableLayout } from '@hatiolab/things-scene'
5
+
6
+ const NATURE = {
7
+ mutable: false,
8
+ resizable: true,
9
+ rotatable: true,
10
+ properties: [
11
+ {
12
+ type: 'number',
13
+ label: 'rows',
14
+ name: 'rows'
15
+ },
16
+ {
17
+ type: 'number',
18
+ label: 'columns',
19
+ name: 'columns'
20
+ },
21
+ {
22
+ type: 'select',
23
+ label: 'direction',
24
+ name: 'direction',
25
+ property: {
26
+ options: [
27
+ {
28
+ display: 'Horizontal',
29
+ value: 'horizontal'
30
+ },
31
+ {
32
+ display: 'Vertical',
33
+ value: 'vertical'
34
+ }
35
+ ]
36
+ }
37
+ },
38
+ {
39
+ type: 'number',
40
+ label: 'round',
41
+ name: 'round'
42
+ },
43
+ {
44
+ type: 'legend-status',
45
+ label: '',
46
+ name: 'status'
47
+ }
48
+ ],
49
+ help: 'scene/component/legend'
50
+ }
51
+
52
+ var controlHandler = {
53
+ ondragmove: function (point: POSITION, index: number, component: Component) {
54
+ var { left, top, width, height } = component.model
55
+ /*
56
+ * point의 좌표는 부모 레이어 기준의 x, y 값이다.
57
+ * 따라서, 도형의 회전을 감안한 좌표로의 변환이 필요하다.
58
+ * Transcoord시에는 point좌표가 부모까지 transcoord되어있는 상태이므로,
59
+ * 컴포넌트자신에 대한 transcoord만 필요하다.(마지막 파라미터를 false로).
60
+ */
61
+ var transcoorded = component.transcoordP2S(point.x, point.y)
62
+ var round = ((transcoorded.x - left) / (width / 2)) * 100
63
+
64
+ round = roundSet(round, width, height)
65
+
66
+ component.set({ round })
67
+ }
68
+ }
69
+
70
+ function roundSet(round: number, width: number, height: number) {
71
+ var max = width > height ? (height / width) * 100 : 100
72
+
73
+ if (round >= max) round = max
74
+ else if (round <= 0) round = 0
75
+
76
+ return round
77
+ }
78
+
79
+ export default class Legend extends Container {
80
+ ready() {
81
+ this.rebuildLegendItems()
82
+ }
83
+
84
+ get showMoveHandle() {
85
+ return false
86
+ }
87
+
88
+ render(context: CanvasRenderingContext2D) {
89
+ var { round = 0 } = this.model
90
+
91
+ var { left, top, width, height } = this.bounds
92
+
93
+ // 박스 그리기
94
+ context.beginPath()
95
+
96
+ round = roundSet(round, width, height)
97
+
98
+ if (round > 0) {
99
+ var radius = (round / 100) * (width / 2)
100
+
101
+ context.moveTo(left + radius, top)
102
+ context.lineTo(left + width - radius, top)
103
+ context.quadraticCurveTo(left + width, top, left + width, top + radius)
104
+ context.lineTo(left + width, top + height - radius)
105
+ context.quadraticCurveTo(left + width, top + height, left + width - radius, top + height)
106
+ context.lineTo(left + radius, top + height)
107
+ context.quadraticCurveTo(left, top + height, left, top + height - radius)
108
+ context.lineTo(left, top + radius)
109
+ context.quadraticCurveTo(left, top, left + radius, top)
110
+
111
+ this.model.padding = {
112
+ top: round / 2,
113
+ left: round / 2,
114
+ right: round / 2,
115
+ bottom: round / 2
116
+ }
117
+ } else {
118
+ context.rect(left, top, width, height)
119
+ }
120
+
121
+ this.drawFill(context)
122
+ this.drawStroke(context)
123
+ }
124
+
125
+ get controls() {
126
+ var { left, top, width, round, height } = this.model
127
+ round = round == undefined ? 0 : roundSet(round, width, height)
128
+
129
+ return [
130
+ {
131
+ x: left + (width / 2) * (round / 100),
132
+ y: top,
133
+ handler: controlHandler
134
+ }
135
+ ]
136
+ }
137
+
138
+ get layout() {
139
+ return TableLayout
140
+ }
141
+
142
+ get nature() {
143
+ return NATURE
144
+ }
145
+
146
+ rebuildLegendItems() {
147
+ if (this.components.length) {
148
+ this.components.slice().forEach(m => m.dispose())
149
+ }
150
+
151
+ var {
152
+ left,
153
+ top,
154
+ width,
155
+ height,
156
+ fillStyle,
157
+ strokeStyle,
158
+ fontColor,
159
+ fontFamily,
160
+ fontSize,
161
+ lineHeight,
162
+ textAlign = 'left',
163
+ round = 0,
164
+ italic,
165
+ bold,
166
+ lineWidth = 0,
167
+ rows,
168
+ columns,
169
+ status = {}
170
+ } = this.model
171
+
172
+ let statusRanges: {
173
+ min: string
174
+ max: string
175
+ description: string
176
+ color: string
177
+ }[] = status.ranges || []
178
+
179
+ var count = statusRanges.length
180
+
181
+ this.add(
182
+ statusRanges.map(range =>
183
+ Model.compile({
184
+ type: 'legend-item',
185
+ text: range.description || `${range.min || ''} ~ ${range.max || ''}`,
186
+ width: 1,
187
+ height: 1,
188
+ color: range.color,
189
+ fontColor,
190
+ fontFamily,
191
+ fontSize,
192
+ lineHeight,
193
+ italic,
194
+ bold,
195
+ textAlign
196
+ })
197
+ )
198
+ )
199
+
200
+ var rows, columns
201
+
202
+ if (!columns && !rows) {
203
+ rows = count
204
+ columns = 1
205
+ } else if (columns && !rows) {
206
+ rows = Math.ceil(count / Number(columns))
207
+ } else if (rows && !columns) {
208
+ columns = Math.ceil(count / Number(rows))
209
+ }
210
+
211
+ this.set({
212
+ layoutConfig: {
213
+ rows,
214
+ columns
215
+ }
216
+ })
217
+ }
218
+
219
+ get hasTextProperty() {
220
+ return true
221
+ }
222
+
223
+ get textHidden() {
224
+ return true
225
+ }
226
+
227
+ onchange(after: Properties, before: Properties) {
228
+ this.rebuildLegendItems()
229
+ }
230
+ }
231
+
232
+ Component.register('legend', Legend)
@@ -0,0 +1,3 @@
1
+ import legend from './legend'
2
+
3
+ export default [legend]
@@ -0,0 +1,19 @@
1
+ import icon from '../../assets/legend.png'
2
+
3
+ export default {
4
+ type: 'legend',
5
+ description: 'legend for visualizer',
6
+ group: 'warehouse' /* line|shape|textAndMedia|chartAndGauge|table|container|dataSource|IoT|3D|warehouse|form|etc */,
7
+ icon,
8
+ model: {
9
+ type: 'legend',
10
+ left: 100,
11
+ top: 100,
12
+ width: 200,
13
+ height: 150,
14
+ fillStyle: '#efefef',
15
+ direction: 'vertical',
16
+ strokeStyle: 'rgba(0, 0, 0, 0.3)',
17
+ lineWidth: 1
18
+ }
19
+ }