@operato/scene-legend 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
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
+ }