@patternfly-java/charts 0.0.2 → 0.0.3

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.
@@ -13,123 +13,394 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { LitElement, html, css } from 'lit';
16
+ import {css, html, LitElement} from 'lit';
17
17
  import React from 'react';
18
- import { createRoot } from 'react-dom/client';
18
+ import {createRoot} from 'react-dom/client';
19
19
 
20
- // Utility: dash-case to camelCase
21
- export const dashToCamel = (str) => str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
20
+ // ------------------------------------------------------ static helpers
22
21
 
23
- // Utility: parse attribute values to JS types
22
+ // Parse attribute values to JS types
24
23
  export const parseAttrValue = (name, value) => {
25
- if (value === '' || value === undefined || value === null) {
26
- // boolean attribute presence => true
27
- return true;
28
- }
29
- const trimmed = String(value).trim();
30
- if (trimmed === 'true') return true;
31
- if (trimmed === 'false') return false;
32
- if (/^\d+(?:\.\d+)?$/.test(trimmed)) return Number(trimmed);
33
- // JSON arrays or objects
34
- if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
35
- try {
36
- return JSON.parse(trimmed);
37
- } catch (_) {
38
- // fallthrough
39
- }
40
- }
41
- return value;
24
+ if (value === '' || value === undefined || value === null) {
25
+ // boolean attribute presence => true
26
+ return true;
27
+ }
28
+ const trimmed = String(value).trim();
29
+ if (trimmed === 'true') return true;
30
+ if (trimmed === 'false') return false;
31
+ if (/^\d+(?:\.\d+)?$/.test(trimmed)) return Number(trimmed);
32
+ // JSON arrays or objects
33
+ if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
34
+ try {
35
+ return JSON.parse(trimmed);
36
+ } catch (_) {
37
+ // fallthrough
38
+ }
39
+ }
40
+ return value;
42
41
  };
43
42
 
44
- // Utility: build props from attributes of element
45
- export const buildPropsFromAttributes = (el) => {
46
- const props = {};
47
- for (const attr of el.attributes) {
48
- const camel = dashToCamel(attr.name);
49
- // skip standard attributes not relevant to React props
50
- if (camel === 'class' || camel === 'style') continue;
51
- props[camel] = parseAttrValue(camel, attr.value);
52
- }
53
- return props;
43
+ // Build props from attributes of an element
44
+ const _buildPropsFromAttributes = (el) => {
45
+ const props = {};
46
+ for (const attr of el.attributes) {
47
+ const camel = _dashToCamel(attr.name);
48
+ // skip standard attributes not relevant to React props
49
+ if (camel === 'class' || camel === 'style') continue;
50
+ props[camel] = parseAttrValue(camel, attr.value);
51
+ }
52
+ return props;
54
53
  };
55
54
 
56
- // Props we currently do not support because they expect functions or React elements
55
+ const _dashToCamel = (str) => str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
56
+
57
+ // Props we currently do not support because they expect React elements
57
58
  const disallowedProps = new Set([
58
- 'events',
59
- 'containerComponent',
60
- 'legendComponent',
61
- 'labelComponent',
62
- 'titleComponent',
63
- 'subTitleComponent',
64
- 'groupComponent',
65
- 'desc' // container-composed
59
+ 'containerComponent',
60
+ 'desc',
61
+ 'events',
62
+ 'groupComponent',
63
+ 'labelComponent',
64
+ 'legendComponent',
65
+ 'subTitleComponent',
66
+ 'titleComponent',
66
67
  ]);
67
68
 
69
+ // ------------------------------------------------------ instance
70
+
68
71
  export class ReactWrapperElement extends LitElement {
69
- static styles = css`
70
- :host { display: inline-block; }
71
- .container { width: 100%; height: 100%; }
72
- `;
73
-
74
- constructor() {
75
- super();
76
- this._root = null;
77
- this._container = null;
78
- this._observer = null;
79
- this._component = null;
80
- }
81
-
82
- createRenderRoot() {
83
- // Use shadow root to encapsulate, but allow React to mount inside
84
- return super.createRenderRoot();
85
- }
86
-
87
- render() {
88
- return html`<div class="container" part="container"></div>`;
89
- }
90
-
91
- firstUpdated() {
92
- this._container = this.renderRoot.querySelector('.container');
93
- this._root = createRoot(this._container);
94
- // Observe attribute changes dynamically (no need for observedAttributes)
95
- this._observer = new MutationObserver(() => this._renderReact());
96
- this._observer.observe(this, { attributes: true });
97
- this._renderReact();
98
- }
99
-
100
- disconnectedCallback() {
101
- if (this._observer) {
102
- this._observer.disconnect();
103
- this._observer = null;
104
- }
105
- if (this._root) {
106
- this._root.unmount();
107
- this._root = null;
108
- }
109
- super.disconnectedCallback();
110
- }
111
-
112
- // Implemented by subclasses to return [ReactComponent, extraProps]
113
- getReactComponent() {
114
- throw new Error('getReactComponent() must be implemented by subclass');
115
- }
116
-
117
- _renderReact() {
118
- if (!this._root) return;
119
- const [Component, extraProps = {}] = this.getReactComponent();
120
- const rawProps = buildPropsFromAttributes(this);
121
- const props = { ...rawProps, ...extraProps };
122
-
123
- // Drop disallowed props and likely-function strings
124
- for (const key of Object.keys(props)) {
125
- if (disallowedProps.has(key)) delete props[key];
126
- }
127
-
128
- // Ensure numeric sizing, default size if none set via style
129
- if (props.width) props.width = Number(props.width);
130
- if (props.height) props.height = Number(props.height);
131
-
132
- // Render React component
133
- this._root.render(React.createElement(Component, props));
134
- }
72
+ static styles = css`
73
+ :host {
74
+ display: inline-block;
75
+ }
76
+
77
+ .container {
78
+ width: 100%;
79
+ height: 100%;
80
+ }
81
+ `;
82
+
83
+ constructor() {
84
+ super();
85
+ this._root = null;
86
+ this._container = null;
87
+ this._observer = null;
88
+ this._component = null;
89
+
90
+ // Common properties applicable to all React chart components that are
91
+ // a) either complex attributes such as a function or structured (JSON) data or
92
+ // b) trigger a re-render when changed.
93
+ this._categories = undefined; // string[]
94
+ this._data = undefined; // any | any[]
95
+ this._height = undefined; // number
96
+ this._labels = undefined; // (data: any) => string
97
+ this._legendAllowWrap = undefined; // boolean
98
+ this._legendData = undefined; // { name?: string; symbol?: { fill?: string; type?: string; }; }[]
99
+ this._legendOrientation = undefined; // string
100
+ this._legendPosition = undefined; // string
101
+ this._padding = undefined; // { top?: number; bottom?: number; left?: number; right?: number }
102
+ this._subTitle = undefined; // string
103
+ this._subTitlePosition = undefined; // string
104
+ this._themeColor = undefined; // string
105
+ this._title = undefined; // string
106
+ this._width = undefined; // number
107
+ }
108
+
109
+ // ------------------------------------------------------ render
110
+
111
+ createRenderRoot() {
112
+ // Use shadow root to encapsulate but allow React to mount inside
113
+ return super.createRenderRoot();
114
+ }
115
+
116
+ render() {
117
+ return html`
118
+ <div class="container"></div>
119
+ <slot style="display: none;"></slot>`;
120
+ }
121
+
122
+ // Must be implemented by subclasses to return [ReactComponent, extraProps]
123
+ getReactComponent() {
124
+ throw new Error('getReactComponent() must be implemented by subclass');
125
+ }
126
+
127
+ // central render method for standalone and slotted elements.
128
+ _renderReact() {
129
+ if (!this._root) return;
130
+
131
+ const rawProps = _buildPropsFromAttributes(this);
132
+ const commonProps = this._commonProps();
133
+ const [Component, extraProps = {}] = this.getReactComponent();
134
+ const props = {...rawProps, ...commonProps, ...extraProps};
135
+ for (const key of Object.keys(props)) {
136
+ if (disallowedProps.has(key)) delete props[key];
137
+ }
138
+
139
+ const children = this._getReactChildren();
140
+ this._root.render(React.createElement(Component, props, ...children));
141
+ }
142
+
143
+ _getReactChildren() {
144
+ const slot = this.renderRoot.querySelector('slot');
145
+ if (!slot) return [];
146
+
147
+ const assignedElements = slot.assignedElements();
148
+ const reactChildren = [];
149
+
150
+ for (const el of assignedElements) {
151
+ if (el instanceof ReactWrapperElement) {
152
+ const childRawProps = _buildPropsFromAttributes(el);
153
+ const childCommonProps = el._commonProps(); // Use child's common props, not parent's!
154
+ const [ChildComponent, childExtraProps = {}] = el.getReactComponent();
155
+ const childProps = {...childRawProps, ...childCommonProps, ...childExtraProps};
156
+ for (const key of Object.keys(childProps)) {
157
+ if (disallowedProps.has(key)) delete childProps[key];
158
+ }
159
+ reactChildren.push(React.createElement(ChildComponent, childProps));
160
+ }
161
+ }
162
+ return reactChildren;
163
+ }
164
+
165
+ // ------------------------------------------------------ lifecycle
166
+
167
+ firstUpdated(_changedProperties) {
168
+ this._container = this.renderRoot.querySelector('.container');
169
+
170
+ // Check if this element is slotted into a parent that will handle rendering
171
+ const isSlotted = this._isSlottedChild();
172
+
173
+ if (isSlotted) {
174
+ // If slotted, DON'T create a React root - parent will handle rendering
175
+ this._observer = new MutationObserver(() => {
176
+ const parent = this.parentElement;
177
+ if (parent && parent instanceof ReactWrapperElement && parent._renderReact) {
178
+ parent._renderReact();
179
+ }
180
+ });
181
+ this._observer.observe(this, {attributes: true});
182
+ } else {
183
+ // If standalone, create React root and render normally
184
+ this._root = createRoot(this._container);
185
+ this._observer = new MutationObserver(() => this._renderReact());
186
+ this._observer.observe(this, {attributes: true, childList: true, subtree: true});
187
+
188
+ // Listen to slot changes to re-render when children change
189
+ const slot = this.renderRoot.querySelector('slot');
190
+ if (slot) {
191
+ slot.addEventListener('slotchange', () => this._renderReact());
192
+ }
193
+ this._renderReact();
194
+ }
195
+ }
196
+
197
+ disconnectedCallback() {
198
+ if (this._observer) {
199
+ this._observer.disconnect();
200
+ this._observer = null;
201
+ }
202
+ if (this._root) {
203
+ this._root.unmount();
204
+ this._root = null;
205
+ }
206
+ super.disconnectedCallback();
207
+ }
208
+
209
+ _isSlottedChild() {
210
+ // Check if this element's parent is also a ReactWrapperElement
211
+ const parent = this.parentElement;
212
+ return parent && parent instanceof ReactWrapperElement;
213
+ }
214
+
215
+ _notifyChange() {
216
+ const parent = this.parentElement;
217
+ if (parent && parent instanceof ReactWrapperElement && parent._renderReact) {
218
+ // If slotted, notify the parent to re-render
219
+ parent._renderReact();
220
+ } else if (this._renderReact) {
221
+ // If standalone, render self
222
+ this._renderReact();
223
+ }
224
+ }
225
+
226
+ // ------------------------------------------------------ getters/setters
227
+
228
+ get categories() {
229
+ return this._categories;
230
+ }
231
+
232
+ set categories(value) {
233
+ this._categories = value;
234
+ this._notifyChange();
235
+ }
236
+
237
+ get data() {
238
+ return this._data;
239
+ }
240
+
241
+ set data(value) {
242
+ this._data = value;
243
+ this._notifyChange();
244
+ }
245
+
246
+ get height() {
247
+ return this._height;
248
+ }
249
+
250
+ set height(value) {
251
+ this._height = value;
252
+ this._notifyChange();
253
+ }
254
+
255
+ get labels() {
256
+ return this._labels;
257
+ }
258
+
259
+ set labels(value) {
260
+ this._labels = value;
261
+ this._notifyChange();
262
+ }
263
+
264
+ get legendAllowWrap() {
265
+ return this._legendAllowWrap;
266
+ }
267
+
268
+ set legendAllowWrap(value) {
269
+ this._legendAllowWrap = value;
270
+ this._notifyChange();
271
+ }
272
+
273
+ get legendData() {
274
+ return this._legendData;
275
+ }
276
+
277
+ set legendData(value) {
278
+ this._legendData = value;
279
+ this._notifyChange();
280
+ }
281
+
282
+ get legendOrientation() {
283
+ return this._legendOrientation;
284
+ }
285
+
286
+ set legendOrientation(value) {
287
+ this._legendOrientation = value;
288
+ this._notifyChange();
289
+ }
290
+
291
+ get legendPosition() {
292
+ return this._legendPosition;
293
+ }
294
+
295
+ set legendPosition(value) {
296
+ this._legendPosition = value;
297
+ this._notifyChange();
298
+ }
299
+
300
+ get padding() {
301
+ return this._padding;
302
+ }
303
+
304
+ set padding(value) {
305
+ this._padding = value;
306
+ this._notifyChange();
307
+ }
308
+
309
+ get subTitle() {
310
+ return this._subTitle;
311
+ }
312
+
313
+ set subTitle(value) {
314
+ this._subTitle = value;
315
+ this._notifyChange();
316
+ }
317
+
318
+ get subTitlePosition() {
319
+ return this._subTitlePosition;
320
+ }
321
+
322
+ set subTitlePosition(value) {
323
+ this._subTitlePosition = value;
324
+ this._notifyChange();
325
+ }
326
+
327
+ get themeColor() {
328
+ return this._themeColor;
329
+ }
330
+
331
+ set themeColor(value) {
332
+ this._themeColor = value;
333
+ this._notifyChange();
334
+ }
335
+
336
+ get title() {
337
+ return this._title;
338
+ }
339
+
340
+ set title(value) {
341
+ this._title = value;
342
+ this._notifyChange();
343
+ }
344
+
345
+ get width() {
346
+ return this._width;
347
+ }
348
+
349
+ set width(value) {
350
+ this._width = value;
351
+ this._notifyChange();
352
+ }
353
+
354
+ _commonProps() {
355
+ const commonProps = {};
356
+ if (this._categories && typeof this._categories !== 'string') {
357
+ commonProps.categories = this._categories;
358
+ } else if (this.getAttribute('categories')) {
359
+ commonProps.categories = parseAttrValue('categories', this.getAttribute('categories'));
360
+ }
361
+ if (this._data && typeof this._data !== 'string') {
362
+ commonProps.data = this._data;
363
+ } else if (this.getAttribute('data')) {
364
+ commonProps.data = parseAttrValue('data', this.getAttribute('data'));
365
+ }
366
+ if (this._height !== undefined) {
367
+ commonProps.height = Number(this._height);
368
+ }
369
+ if (this._labels !== undefined) {
370
+ commonProps.labels = this._labels;
371
+ }
372
+ if (this._legendAllowWrap !== undefined) {
373
+ commonProps.legendAllowWrap = this._legendAllowWrap;
374
+ }
375
+ if (this._legendData && typeof this._legendData !== 'string') {
376
+ commonProps.legendData = this._legendData;
377
+ } else if (this.getAttribute('legend-data')) {
378
+ commonProps.legendData = parseAttrValue('legend-data', this.getAttribute('legend-data'));
379
+ }
380
+ if (this._legendOrientation !== undefined) {
381
+ commonProps.legendOrientation = this._legendOrientation;
382
+ }
383
+ if (this._legendPosition !== undefined) {
384
+ commonProps.legendPosition = this._legendPosition;
385
+ }
386
+ if (this._padding !== undefined) {
387
+ commonProps.padding = this._padding;
388
+ }
389
+ if (this._subTitle !== undefined) {
390
+ commonProps.subTitle = this._subTitle;
391
+ }
392
+ if (this._subTitlePosition !== undefined) {
393
+ commonProps.subTitlePosition = this._subTitlePosition;
394
+ }
395
+ if (this._themeColor !== undefined) {
396
+ commonProps.themeColor = this._themeColor;
397
+ }
398
+ if (this._title !== undefined) {
399
+ commonProps.title = this._title;
400
+ }
401
+ if (this._width !== undefined) {
402
+ commonProps.width = Number(this._width);
403
+ }
404
+ return commonProps;
405
+ }
135
406
  }
Binary file
Binary file
@@ -1,49 +0,0 @@
1
- /*
2
- * Copyright 2023 Red Hat
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * https://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
- import { ReactWrapperElement, parseAttrValue } from '../react-wrapper.js';
17
- import { ChartBullet } from '@patternfly/react-charts/victory';
18
-
19
- export class PfChartBullet extends ReactWrapperElement {
20
- getReactComponent() {
21
- const extraProps = {};
22
- // defaults matching examples
23
- if (!this.width) extraProps.width = 600;
24
- if (!this.height) extraProps.height = 150;
25
-
26
- // allow setting various data props via property assignment or data-* attributes in JSON
27
- const keys = [
28
- 'data',
29
- 'primaryDotMeasureData',
30
- 'primarySegmentedMeasureData',
31
- 'comparativeErrorMeasureData',
32
- 'comparativeWarningMeasureData',
33
- 'qualitativeRangeData',
34
- 'legendData'
35
- ];
36
- for (const key of keys) {
37
- const attr = this.getAttribute(key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`));
38
- if (this[key] && typeof this[key] !== 'string') {
39
- extraProps[key] = this[key];
40
- } else if (attr) {
41
- extraProps[key] = parseAttrValue(key, attr);
42
- }
43
- }
44
-
45
- return [ChartBullet, extraProps];
46
- }
47
- }
48
-
49
- customElements.define('pf-chart-bullet', PfChartBullet);
@@ -1,34 +0,0 @@
1
- /*
2
- * Copyright 2023 Red Hat
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * https://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
- import { ReactWrapperElement, parseAttrValue } from '../react-wrapper.js';
17
- import { ChartDonutUtilization } from '@patternfly/react-charts/victory';
18
-
19
- export class PfChartDonutUtilization extends ReactWrapperElement {
20
- getReactComponent() {
21
- const extraProps = {};
22
- if (!this.width) extraProps.width = 230;
23
- if (!this.height) extraProps.height = 230;
24
-
25
- if (this.data && typeof this.data !== 'string') {
26
- extraProps.data = this.data;
27
- } else if (this.getAttribute('data')) {
28
- extraProps.data = parseAttrValue('data', this.getAttribute('data'));
29
- }
30
- return [ChartDonutUtilization, extraProps];
31
- }
32
- }
33
-
34
- customElements.define('pf-chart-donut-utilization', PfChartDonutUtilization);
@@ -1,53 +0,0 @@
1
- /*
2
- * Copyright 2023 Red Hat
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * https://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
- import { ReactWrapperElement, parseAttrValue } from '../react-wrapper.js';
17
- import { ChartDonut } from '@patternfly/react-charts/victory';
18
-
19
- export class PfChartDonut extends ReactWrapperElement {
20
- constructor() {
21
- super();
22
- this._labels = undefined;
23
- }
24
-
25
- // Support function-valued 'labels' via direct property assignment on this element only
26
- get labels() {
27
- return this._labels;
28
- }
29
- set labels(fn) {
30
- this._labels = fn;
31
- this._renderReact();
32
- }
33
-
34
- // Provide defaults for common props via extraProps
35
- getReactComponent() {
36
- const extraProps = {};
37
- // allow setting data via property as object (not only attribute)
38
- if (this.data && typeof this.data !== 'string') {
39
- extraProps.data = this.data;
40
- } else if (this.getAttribute('data')) {
41
- // data can be object or array; parse
42
- const parsed = parseAttrValue('data', this.getAttribute('data'));
43
- extraProps.data = parsed;
44
- }
45
- // If a function-valued labels property was assigned, prefer it over attribute value
46
- if (typeof this._labels === 'function') {
47
- extraProps.labels = this._labels;
48
- }
49
- return [ChartDonut, extraProps];
50
- }
51
- }
52
-
53
- customElements.define('pf-chart-donut', PfChartDonut);