virtual-tree-canvas 0.7.0 → 0.7.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.
@@ -1,171 +1,171 @@
1
- # Catálogo de iconos
1
+ # Icon catalog
2
2
 
3
- Los 65 iconos incluidos están disponibles en `resources/icons/`.
4
- Consulta la [galería visual](./icon-catalog.html) para verlos sobre fondos claros
5
- u oscuros y comparar tamaños. `builtinIconNames` permite enumerar sus IDs desde JavaScript.
3
+ The 65 bundled icons are available in `resources/icons/`. Open the
4
+ [rendered visual catalog](https://htmlpreview.github.io/?https://github.com/mploscos/virtual-tree-canvas/blob/main/docs/icon-catalog.html)
5
+ to compare them at different sizes on light and dark backgrounds. The
6
+ [HTML source](./icon-catalog.html) is also available. `builtinIconNames` lists
7
+ their IDs from JavaScript.
6
8
 
7
- ## Uso
9
+ ## Usage
8
10
 
9
- Asigna un ID a la propiedad `icon` del nodo:
11
+ Assign an ID to the node's `icon` property:
10
12
 
11
13
  ```js
12
- view.setData([{ id: 'operator-1', label: 'Operador', icon: 'person' }]);
14
+ view.setData([{ id: 'operator-1', label: 'Operator', icon: 'person' }]);
13
15
  ```
14
16
 
15
- - `data-bus` representa un bus de datos; `bus-vehicle`, un autobús.
16
- - `placeholder` es el icono utilizado cuando no hay uno específico.
17
- - Acompaña los iconos con etiquetas. Para estados como advertencias o errores,
18
- proporciona también texto y no dependas únicamente del color.
19
- - Para añadir un icono propio, usa `view.registerIcon('custom', svgStringOrUrl)`
20
- y asigna `icon: 'custom'` al nodo.
21
- - Configura `iconsBaseUrl` al crear la vista si los recursos se sirven desde otra URL.
22
- Los SVG se cargan al dibujarlos o mediante `view.controller.iconRegistry.prepare()`.
17
+ - `data-bus` represents a data bus; `bus-vehicle` represents a passenger bus.
18
+ - `placeholder` is used when no specific icon is available.
19
+ - Pair icons with labels. For states such as warnings or errors, also provide
20
+ text instead of relying on color alone.
21
+ - To add a custom icon, call `view.registerIcon('custom', svgStringOrUrl)` and
22
+ assign `icon: 'custom'` to the node.
23
+ - Set `iconsBaseUrl` when creating the view if assets are served from another URL.
24
+ SVGs are loaded when drawn or through `view.controller.iconRegistry.prepare()`.
23
25
 
24
- ## Iconos disponibles
26
+ ## Available icons
25
27
 
26
- ### Personas y organización
28
+ ### People and organization
27
29
 
28
- | ID | Significado |
30
+ | ID | Meaning |
29
31
  | --- | --- |
30
- | `person` | Persona |
31
- | `people` | Grupo |
32
- | `child` | Infancia |
32
+ | `person` | Person |
33
+ | `people` | Group |
34
+ | `child` | Child |
33
35
 
34
- ### Animales
36
+ ### Animals
35
37
 
36
- | ID | Significado |
38
+ | ID | Meaning |
37
39
  | --- | --- |
38
- | `dog` | Perro |
39
- | `cat` | Gato |
40
- | `bird` | Ave |
41
- | `fish` | Pez |
42
- | `insect` | Insecto |
40
+ | `dog` | Dog |
41
+ | `cat` | Cat |
42
+ | `bird` | Bird |
43
+ | `fish` | Fish |
44
+ | `insect` | Insect |
43
45
 
44
- ### Plantas y ecosistemas
46
+ ### Plants and ecosystems
45
47
 
46
- | ID | Significado |
48
+ | ID | Meaning |
47
49
  | --- | --- |
48
- | `tree` | Árbol |
49
- | `leaf` | Hoja |
50
- | `flower` | Flor |
51
- | `seedling` | Brote |
50
+ | `tree` | Tree |
51
+ | `leaf` | Leaf |
52
+ | `flower` | Flower |
53
+ | `seedling` | Seedling |
52
54
 
53
- ### Tiempo atmosférico
55
+ ### Weather
54
56
 
55
- | ID | Significado |
57
+ | ID | Meaning |
56
58
  | --- | --- |
57
- | `sun` | Sol |
58
- | `moon` | Luna |
59
- | `cloud` | Nube |
60
- | `rain` | Lluvia |
61
- | `snow` | Nieve |
62
- | `wind` | Viento |
63
- | `storm` | Tormenta |
64
- | `fog` | Niebla |
59
+ | `sun` | Sun |
60
+ | `moon` | Moon |
61
+ | `cloud` | Cloud |
62
+ | `rain` | Rain |
63
+ | `snow` | Snow |
64
+ | `wind` | Wind |
65
+ | `storm` | Storm |
66
+ | `fog` | Fog |
65
67
 
66
- ### Terreno y agua
68
+ ### Terrain and water
67
69
 
68
- | ID | Significado |
70
+ | ID | Meaning |
69
71
  | --- | --- |
70
- | `ground` | Dominio terrestre |
71
- | `surface` | Dominio de superficie |
72
- | `subsurface` | Dominio submarino |
72
+ | `ground` | Ground domain |
73
+ | `surface` | Surface domain |
74
+ | `subsurface` | Subsurface domain |
73
75
 
74
- ### Transporte terrestre
76
+ ### Ground transportation
75
77
 
76
- | ID | Significado |
78
+ | ID | Meaning |
77
79
  | --- | --- |
78
- | `car` | Coche |
79
- | `bicycle` | Bicicleta |
80
- | `bus-vehicle` | Autobús de pasajeros |
80
+ | `car` | Car |
81
+ | `bicycle` | Bicycle |
82
+ | `bus-vehicle` | Passenger bus |
81
83
 
82
- ### Aviación y espacio
84
+ ### Aviation and space
83
85
 
84
- | ID | Significado |
86
+ | ID | Meaning |
85
87
  | --- | --- |
86
- | `aircraft` | Avión |
87
- | `helicopter` | Helicóptero |
88
- | `drone` | Dron |
89
- | `space` | Dominio espacial |
88
+ | `aircraft` | Aircraft |
89
+ | `helicopter` | Helicopter |
90
+ | `drone` | Drone |
91
+ | `space` | Space domain |
90
92
 
91
- ### Entorno marítimo
93
+ ### Maritime environment
92
94
 
93
- | ID | Significado |
95
+ | ID | Meaning |
94
96
  | --- | --- |
95
- | `ship` | Buque |
97
+ | `ship` | Ship |
96
98
 
97
- ### Objetos e instalaciones
99
+ ### Objects and facilities
98
100
 
99
- | ID | Significado |
101
+ | ID | Meaning |
100
102
  | --- | --- |
101
- | `box` | Caja |
102
- | `building` | Edificio |
103
+ | `box` | Box |
104
+ | `building` | Building |
103
105
 
104
- ### Datos y redes
106
+ ### Data and networks
105
107
 
106
- | ID | Significado |
108
+ | ID | Meaning |
107
109
  | --- | --- |
108
- | `data-bus` | Bus de datos |
109
- | `network` | Red |
110
- | `server` | Servidor |
111
- | `database` | Base de datos |
112
- | `link` | Enlace |
110
+ | `data-bus` | Data bus |
111
+ | `network` | Network |
112
+ | `server` | Server |
113
+ | `database` | Database |
114
+ | `link` | Link |
113
115
 
114
- ### Sensores y mediciones
116
+ ### Sensors and measurements
115
117
 
116
- | ID | Significado |
118
+ | ID | Meaning |
117
119
  | --- | --- |
118
- | `sensor` | Sensor genérico |
120
+ | `sensor` | Generic sensor |
119
121
  | `radar` | Radar |
120
- | `thermometer` | Termómetro |
122
+ | `thermometer` | Thermometer |
121
123
 
122
- ### Geografía y navegación
124
+ ### Geography and navigation
123
125
 
124
- | ID | Significado |
126
+ | ID | Meaning |
125
127
  | --- | --- |
126
- | `globe` | Globo terrestre |
127
- | `point` | Punto |
128
- | `track` | Traza |
128
+ | `globe` | Globe |
129
+ | `point` | Point |
130
+ | `track` | Track |
129
131
 
130
- ### Inspector y documentos
132
+ ### Inspector and documents
131
133
 
132
- | ID | Significado |
134
+ | ID | Meaning |
133
135
  | --- | --- |
134
- | `folder` | Carpeta |
135
- | `inspector-object` | Objeto de datos |
136
+ | `folder` | Folder |
137
+ | `inspector-object` | Data object |
136
138
  | `inspector-array` | Array |
137
- | `inspector-value` | Valor escalar |
139
+ | `inspector-value` | Scalar value |
138
140
 
139
- ### Estados y alertas
141
+ ### States and alerts
140
142
 
141
- | ID | Significado |
143
+ | ID | Meaning |
142
144
  | --- | --- |
143
- | `warning` | Advertencia |
145
+ | `warning` | Warning |
144
146
  | `error` | Error |
145
- | `placeholder` | Tipo desconocido |
146
- | `damage` | Daño |
147
- | `situation` | Situación |
148
- | `lock` | Bloqueado |
147
+ | `placeholder` | Unknown type |
148
+ | `damage` | Damage |
149
+ | `situation` | Situation |
150
+ | `lock` | Locked |
149
151
 
150
- ### Acciones y controles
152
+ ### Actions and controls
151
153
 
152
- | ID | Significado |
154
+ | ID | Meaning |
153
155
  | --- | --- |
154
156
  | `control` | Control |
155
- | `task` | Tarea |
156
- | `star` | Favorito |
157
- | `pin` | Fijado |
158
- | `heart` | Preferencia afectiva |
159
- | `grip` | Asa de arrastre |
157
+ | `task` | Task |
158
+ | `star` | Favorite |
159
+ | `star-filled` | Active favorite |
160
+ | `pin` | Pinned |
161
+ | `heart` | Favorite |
162
+ | `grip` | Drag handle |
163
+ | `trash` | Delete or remove |
160
164
 
161
- ### Tiempo y simulación
165
+ ### Time and simulation
162
166
 
163
- | ID | Significado |
167
+ | ID | Meaning |
164
168
  | --- | --- |
165
- | `clock` | Reloj |
166
- | `calendar` | Calendario |
167
- | `munition` | Munición |
168
-
169
- - `trash`: eliminar un elemento o quitarlo de una lista.
170
-
171
- - `star-filled`: favorito activo; combina con `star` para el estado inactivo.
169
+ | `clock` | Clock |
170
+ | `calendar` | Calendar |
171
+ | `munition` | Munition |
@@ -0,0 +1,38 @@
1
+ # Dynamic update benchmark: 0.7.0 → 0.7.1
2
+
3
+ Measured on 2026-09-13 in headless Chromium on the development host. Each case
4
+ uses 10,000 flat nodes, approximately 40 visible rows, 60 update ticks and 500
5
+ patches per tick (30,000 patches/case). Times are milliseconds. The reproducible
6
+ command is `npm run benchmark:dynamic`.
7
+
8
+ The 0.7.0 measurement used an extracted clean `HEAD` and the same benchmark file.
9
+ Icon-load invalidations and initial ResizeObserver work were settled before each
10
+ sample. “Total avg” is patch application plus render time per tick; a zero-render
11
+ case therefore contains only patch processing.
12
+
13
+ | Sorted | RowActions | Visible patches | Patch avg/p95 before → after | Render avg/p95 before → after | Renders before → after | Total avg before → after |
14
+ |---|---:|---:|---:|---:|---:|---:|
15
+ | no | no | 0% | 0.417/0.700 → 0.788/1.200 | 1.573/2.000 → 0/0 | 60 → 0 | 1.990 → 0.788 |
16
+ | no | no | 10% | 0.413/0.600 → 0.657/0.900 | 1.422/2.100 → 1.485/2.000 | 60 → 60 | 1.835 → 2.142 |
17
+ | no | no | 100% | 0.285/0.400 → 0.212/0.400 | 1.335/2.000 → 1.335/1.900 | 60 → 60 | 1.620 → 1.547 |
18
+ | no | yes | 0% | 0.303/0.500 → 0.635/0.900 | 2.408/3.600 → 0/0 | 60 → 0 | 2.712 → 0.635 |
19
+ | no | yes | 10% | 0.318/0.500 → 0.592/0.800 | 2.708/3.400 → 2.960/3.900 | 60 → 60 | 3.027 → 3.552 |
20
+ | no | yes | 100% | 0.203/0.300 → 0.153/0.300 | 2.535/3.600 → 2.552/3.700 | 60 → 60 | 2.738 → 2.705 |
21
+ | yes | no | 0% | 0.317/0.400 → 0.548/0.800 | 2.012/3.400 → 0/0 | 60 → 0 | 2.328 → 0.548 |
22
+ | yes | no | 10% | 0.345/0.500 → 0.582/0.800 | 1.945/2.600 → 1.343/2.300 | 60 → 60 | 2.290 → 1.925 |
23
+ | yes | no | 100% | 0.267/0.500 → 0.220/0.400 | 2.005/2.600 → 1.305/2.300 | 60 → 60 | 2.272 → 1.525 |
24
+ | yes | yes | 0% | 0.287/0.400 → 0.493/0.700 | 3.087/3.900 → 0/0 | 60 → 0 | 3.373 → 0.493 |
25
+ | yes | yes | 10% | 0.307/0.500 → 0.478/0.800 | 3.402/5.200 → 2.782/4.100 | 60 → 60 | 3.708 → 3.260 |
26
+ | yes | yes | 100% | 0.182/0.300 → 0.137/0.300 | 3.103/4.800 → 2.447/3.800 | 60 → 60 | 3.285 → 2.583 |
27
+
28
+ In every 0%-visible case, 0.7.1 recorded 60 renders avoided because the changes
29
+ were offscreen, with zero render requests and executions. In visible cases,
30
+ 0.7.0 performed 60 full RowActions synchronizations; 0.7.1 performed 60
31
+ incremental and zero full synchronizations. At 100% visible, repeated patches for
32
+ the approximately 40 visible IDs coalesced 30,000 received patches into 2,400
33
+ unique changed nodes across the 60 frames.
34
+
35
+ The queue adds measurable bookkeeping when nearly all 500 IDs are unique and a
36
+ render is required anyway; this is visible in the two unsorted 10% cases. The
37
+ largest gains are the intended real-time-tree case (offscreen traffic), and active
38
+ sorting additionally benefits from removing the 10,000-entry scene copy.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "virtual-tree-canvas",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "High-performance Canvas2D virtual tree/table widget for very large hierarchical datasets.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -34,7 +34,8 @@
34
34
  "scripts": {
35
35
  "demo": "python3 -m http.server 4173 --bind localhost",
36
36
  "test": "node --test test/*.test.js",
37
- "test:browser": "node test/run-browser.mjs"
37
+ "test:browser": "node test/run-browser.mjs",
38
+ "benchmark:dynamic": "node scripts/diagnostics/profile-dynamic-updates.mjs"
38
39
  },
39
40
  "keywords": [
40
41
  "tree",
@@ -1,9 +1,15 @@
1
- /** Resolve built-in SVG resources independently of the bundled JavaScript location.
1
+ /**
2
+ * Resolve built-in SVG resources independently of the bundled JavaScript location.
2
3
  * @param {string | URL} [iconsBaseUrl] Directory URL, with or without a trailing slash.
3
4
  */
4
5
  export function createBuiltinIconUrls(iconsBaseUrl) {
5
- const base = String(iconsBaseUrl ?? new URL('../../resources/icons/', import.meta.url)).replace(/\/+$/, '');
6
- return Object.freeze(Object.fromEntries(builtinIconNames.map(name => [name, `${base}/${name}.svg`])));
6
+ const base = String(iconsBaseUrl ?? new URL('../../resources/icons/', import.meta.url)).replace(
7
+ /\/+$/,
8
+ ''
9
+ );
10
+ return Object.freeze(
11
+ Object.fromEntries(builtinIconNames.map((name) => [name, `${base}/${name}.svg`]))
12
+ );
7
13
  }
8
14
 
9
15
  export const builtinIconNames = Object.freeze([
@@ -71,5 +77,5 @@ export const builtinIconNames = Object.freeze([
71
77
  'track',
72
78
  'tree',
73
79
  'warning',
74
- 'wind',
80
+ 'wind'
75
81
  ]);
@@ -1,44 +1,34 @@
1
1
  const hasOwn = Object.prototype.hasOwnProperty;
2
2
 
3
- const knownDynamicStateKeys = new Set([
4
- 'value',
5
- 'status',
6
- 'progress',
7
- 'pulse',
8
- 'color',
9
- 'selected',
10
- 'highlighted',
11
- 'visible',
12
- 'updated',
13
- 'updatedAt',
14
- 'icon',
15
- ]);
16
-
17
3
  /**
18
4
  * @param {import('./types.js').NodeDynamicState} target
19
5
  * @param {import('./types.js').NodeDynamicState} state
20
6
  */
21
7
  export function mergeDynamicState(target, state) {
22
- if (!state) return target;
8
+ mergeDynamicStateChanged(target, state);
9
+ return target;
10
+ }
23
11
 
24
- if (hasOwn.call(state, 'value') && target.value !== state.value) target.value = state.value;
25
- if (hasOwn.call(state, 'status') && target.status !== state.status) target.status = state.status;
26
- if (hasOwn.call(state, 'progress') && target.progress !== state.progress) target.progress = state.progress;
27
- if (hasOwn.call(state, 'pulse') && target.pulse !== state.pulse) target.pulse = state.pulse;
28
- if (hasOwn.call(state, 'color') && target.color !== state.color) target.color = state.color;
29
- if (hasOwn.call(state, 'selected') && target.selected !== state.selected) target.selected = state.selected;
30
- if (hasOwn.call(state, 'highlighted') && target.highlighted !== state.highlighted) target.highlighted = state.highlighted;
31
- if (hasOwn.call(state, 'visible') && target.visible !== state.visible) target.visible = state.visible;
32
- if (hasOwn.call(state, 'updated') && target.updated !== state.updated) target.updated = state.updated;
33
- if (hasOwn.call(state, 'updatedAt') && target.updatedAt !== state.updatedAt) target.updatedAt = state.updatedAt;
34
- if (hasOwn.call(state, 'icon') && target.icon !== state.icon) target.icon = state.icon;
12
+ /**
13
+ * Merge a dynamic state patch and report whether at least one own property changed.
14
+ * Values are compared with Object.is so repeated NaN values and signed zero are
15
+ * handled consistently.
16
+ *
17
+ * @param {import('./types.js').NodeDynamicState} target
18
+ * @param {import('./types.js').NodeDynamicState} state
19
+ * @returns {boolean}
20
+ */
21
+ export function mergeDynamicStateChanged(target, state) {
22
+ if (!state) return false;
23
+ let changed = false;
35
24
 
36
25
  for (const key in state) {
37
- if (!hasOwn.call(state, key) || knownDynamicStateKeys.has(key)) continue;
38
- if (target[key] !== state[key]) target[key] = state[key];
26
+ if (!hasOwn.call(state, key) || Object.is(target[key], state[key])) continue;
27
+ target[key] = state[key];
28
+ changed = true;
39
29
  }
40
30
 
41
- return target;
31
+ return changed;
42
32
  }
43
33
 
44
34
  /** @param {import('./types.js').NodeDynamicState} state */
@@ -24,6 +24,10 @@ export class PatchBatcher {
24
24
  return patches;
25
25
  }
26
26
 
27
+ clear() {
28
+ this.pending.clear();
29
+ }
30
+
27
31
  get size() {
28
32
  return this.pending.size;
29
33
  }