@kubex/zinc 1.0.106 → 1.0.107

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,208 +1,217 @@
1
- import {type CSSResultGroup, html, type PropertyValues, unsafeCSS} from 'lit';
2
- import {property} from 'lit/decorators.js';
3
- import ApexCharts from "apexcharts";
4
- import ZincElement from '../../internal/zinc-element';
5
-
1
+ import * as echarts from 'echarts/core';
2
+ import { BarChart, LineChart, SankeyChart } from 'echarts/charts';
3
+ import {
4
+ buildAreaOption,
5
+ buildBarOption,
6
+ type BuilderProps,
7
+ buildLineOption,
8
+ buildSankeyOption,
9
+ type ChartType,
10
+ type SeriesItem,
11
+ } from './builders';
12
+ import { CanvasRenderer } from 'echarts/renderers';
13
+ import { type CSSResultGroup, html, type PropertyValues, unsafeCSS } from 'lit';
14
+ import {
15
+ DataZoomComponent,
16
+ GridComponent,
17
+ LegendComponent,
18
+ TitleComponent,
19
+ TooltipComponent,
20
+ } from 'echarts/components';
21
+ import { property } from 'lit/decorators.js';
6
22
  import styles from './chart.scss';
23
+ import ZincElement from '../../internal/zinc-element';
24
+ import type { ECharts } from 'echarts/core';
25
+
26
+ echarts.use([
27
+ BarChart,
28
+ LineChart,
29
+ SankeyChart,
30
+ GridComponent,
31
+ TooltipComponent,
32
+ LegendComponent,
33
+ TitleComponent,
34
+ DataZoomComponent,
35
+ CanvasRenderer,
36
+ ]);
7
37
 
8
38
  /**
9
- * @summary Short summary of the component's intended use.
39
+ * @summary Chart component powered by Apache ECharts.
10
40
  * @documentation https://zinc.style/components/data-chart
11
41
  * @status experimental
12
42
  * @since 1.0
13
43
  *
14
- * @dependency zn-example
15
- *
16
- * @event zn-event-name - Emitted as an example.
17
- *
18
- * @slot - The default slot.
19
- * @slot example - An example slot.
20
- *
21
44
  * @csspart base - The component's base wrapper.
22
- *
23
- * @cssproperty --example - An example CSS custom property.
24
45
  */
25
46
  export default class ZnChart extends ZincElement {
26
47
  static styles: CSSResultGroup = unsafeCSS(styles);
27
48
 
28
- @property() type: 'area' | 'bar' | 'line' = 'bar';
29
- @property({type: Array}) data: any[] = [];
30
- @property({type: Array}) categories: string | string[] = '';
31
-
32
- @property() xAxis: string;
33
- @property({type: Number, attribute: 'd-size'}) datapointSize: number = 1;
34
- @property({type: Boolean}) stacked = false;
49
+ @property() type: ChartType = 'bar';
50
+ @property({ type: Array }) data: SeriesItem[] = [];
51
+ @property({ type: Array }) categories: string[] = [];
52
+
53
+ @property({ attribute: 'xaxis' }) xAxis: 'datetime' | 'category' | 'numeric';
54
+ @property({ type: Number, attribute: 'd-size' }) datapointSize: number = 1;
55
+ @property({ type: Boolean }) stacked = false;
56
+
57
+ @property({ type: Boolean }) live = false;
58
+ @property({ attribute: 'data-url' }) dataUrl = '';
59
+ @property({ attribute: 'live-interval', type: Number }) liveInterval = 1000;
60
+ @property({ type: Number, reflect: true }) height = 300;
61
+
62
+ @property({
63
+ attribute: 'enable-animations',
64
+ converter: {
65
+ fromAttribute: (value: string | null) => {
66
+ if (value === null) return false;
67
+ if (value === '' || value === 'true') return true;
68
+ const num = parseFloat(value);
69
+ return Number.isNaN(num) ? true : num;
70
+ },
71
+ },
72
+ }) enableAnimations: boolean | number = false;
73
+ @property({ attribute: 'y-axis-append' }) yAxisAppend: string;
74
+
75
+ @property({ type: Array }) colors?: string[];
76
+ @property({ attribute: 'sync-group' }) syncGroup?: string;
77
+ @property({ type: Boolean }) smooth = false;
78
+ @property({
79
+ converter: {
80
+ fromAttribute: (value: string | null) => {
81
+ if (value === null) return false;
82
+ if (value === '' || value === 'true') return true;
83
+ const num = parseFloat(value);
84
+ return Number.isNaN(num) ? true : num;
85
+ },
86
+ },
87
+ }) scale: boolean | number = false;
35
88
 
36
- // Live
37
- @property({type: Boolean}) live = false;
38
- @property({attribute: 'data-url'}) dataUrl = '';
39
- @property({attribute: 'live-interval', type: Number}) liveInterval = 1000;
40
- @property({type: Number, reflect: true}) height = 300;
89
+ private chart?: ECharts;
90
+ private resizeObserver?: ResizeObserver;
91
+ private liveTimer?: number;
41
92
 
42
- @property({attribute: 'enable-animations', type: Boolean}) enableAnimations = false;
93
+ protected firstUpdated(_changedProperties: PropertyValues) {
94
+ this.initChart();
95
+ super.firstUpdated(_changedProperties);
96
+ }
43
97
 
44
- @property({attribute: 'y-axis-append'}) yAxisAppend: string;
98
+ private getTheme(): 'light' | 'dark' {
99
+ return this.getAttribute('t') === 'dark' ? 'dark' : 'light';
100
+ }
45
101
 
46
- private chart: ApexCharts;
102
+ private getTextColor(): string | undefined {
103
+ const cs = getComputedStyle(this);
104
+ const rgb = cs.getPropertyValue('--zn-text').trim();
105
+ if (!rgb) return undefined;
106
+ const opacity = cs.getPropertyValue('--zn-text-opacity').trim() || '1';
107
+ return `rgba(${rgb}, ${opacity})`;
108
+ }
47
109
 
48
- // private lightColors = ["#8967ef", "#6483F2", "#29bab5", "#3F51B5", "#9C27B0", "#ff6c9c", "#6836F5", "#47D6D6"];
49
- // private darkColors = ["#703cff", "#3b65fb", "#07e3db", "#5c75ff", "#e12cff", "#ff125f", "#ae90ff", "#a6ff9c"];
110
+ private getBorderColor(): string | undefined {
111
+ const rgb = getComputedStyle(this).getPropertyValue('--zn-border-color').trim();
112
+ return rgb ? `rgb(${rgb})` : undefined;
113
+ }
50
114
 
51
- protected firstUpdated(_changedProperties: PropertyValues) {
52
- const theme = this.getAttribute('t') || 'light';
53
- // Setup Chart and render it
54
- const options = {
55
- animations: this.enableAnimations,
56
- chart: {
57
- type: this.type,
58
- toolbar: {show: false},
59
- foreColor: theme == 'dark' ? "rgb(149, 163, 184)" : "rgb(123, 112, 151)",
60
- height: this.height + 'px',
61
- fontFamily: '"Inter", sans',
62
- stacked: this.stacked,
63
- zoom: {
64
- enabled: false
65
- },
66
- animations: {
67
- enabled: false,
68
- },
69
- events: {}
70
- },
71
- dataLabels: {
72
- enabled: false,
73
- },
74
- markers: {
75
- size: this.datapointSize,
76
- strokeWidth: 0,
77
- hover: {
78
- sizeOffset: 2
79
- },
80
- },
81
- yaxis: {
82
- labels: {
83
- formatter: (value: number) => {
84
- if (this.yAxisAppend) {
85
- return value + this.yAxisAppend;
86
- }
87
- return value;
88
- }
89
- }
90
- },
91
- tooltip: {
92
- theme: theme,
93
- y: {
94
- formatter: (value: number,) => {
95
- if (this.yAxisAppend) {
96
- return value + this.yAxisAppend;
97
- }
98
- return value;
99
- }
100
- }
101
- },
102
- grid: {
103
- borderColor: theme === 'dark' ? "rgb(50, 50, 60)" : 'rgb(224, 221, 233)',
104
- },
105
- //colors: theme === 'dark' ? this.darkColors : this.lightColors,
106
- legend: {
107
- position: 'top',
108
- horizontalAlign: 'right',
109
- fontSize: '12px',
110
- fontFamily: '"Inter", sans',
111
- fontWeight: 400,
112
- labels: {
113
- colors: theme == 'dark' ? "rgb(149, 163, 184)" : "rgb(123, 112, 151)",
114
- },
115
- markers: {
116
- shape: 'circle',
117
- },
118
- },
119
- stroke: {
120
- width: 1.5
121
- },
122
- fill: {
123
- type: 'solid',
124
- opacity: this.type === 'area' ? 0.1 : 0.8
125
- },
126
- series: this.data,
127
- xaxis: {},
115
+ private buildOption() {
116
+ const props: BuilderProps = {
117
+ type: this.type,
118
+ data: this.data ?? [],
119
+ categories: Array.isArray(this.categories) ? this.categories : [],
120
+ xAxisType: this.xAxis,
121
+ yAxisAppend: this.yAxisAppend,
122
+ stacked: this.stacked,
123
+ enableAnimations: this.enableAnimations,
124
+ datapointSize: this.datapointSize,
125
+ colors: this.colors,
126
+ theme: this.getTheme(),
127
+ smooth: this.smooth,
128
+ scale: this.scale,
129
+ textColor: this.getTextColor(),
130
+ borderColor: this.getBorderColor(),
128
131
  };
129
-
130
- if (this.xAxis) {
131
- options.xaxis = {
132
- type: this.xAxis,
133
- };
134
- } else {
135
- options.xaxis = {
136
- categories: this.categories,
137
- };
132
+ switch (this.type) {
133
+ case 'bar': return buildBarOption(props);
134
+ case 'line': return buildLineOption(props);
135
+ case 'area': return buildAreaOption(props);
136
+ case 'sankey': return buildSankeyOption(props);
137
+ default: return buildBarOption(props);
138
138
  }
139
+ }
139
140
 
140
- if (this.live) {
141
- options.chart.animations = {
142
- enabled: true,
143
- // @ts-expect-error this is available. it lies.
144
- easing: 'linear',
145
- dynamicAnimation: {
146
- speed: 400
147
- }
148
- };
149
-
150
- options.chart.events = {
151
- animationEnd: function (chartCtx: any, opts: any) {
152
- const newData1 = chartCtx.w.config.series[0].data.slice();
153
- newData1.shift();
154
- const newData2 = chartCtx.w.config.series[1].data.slice();
155
- newData2.shift();
156
-
157
- // check animation end event for just 1 series to avoid multiple updates
158
- if (opts.el.node.getAttribute('index') === '0') {
159
- window.setTimeout(function () {
160
- chartCtx.updateOptions({
161
- series: [{
162
- data: newData1
163
- }, {
164
- data: newData2
165
- }],
166
- }, false, false);
167
- }, 300);
168
- }
169
-
170
- }
171
- };
172
- }
141
+ private initChart() {
142
+ const host = this.shadowRoot?.getElementById('chart') as HTMLElement | null;
143
+ if (!host) return;
144
+ host.style.height = `${this.height}px`;
145
+ this.chart = echarts.init(host);
146
+ this.chart.setOption(this.buildOption());
173
147
 
174
- if (!this.chart) {
175
- this.chart = new ApexCharts(this.shadowRoot?.getElementById('chart'), options);
176
- this.chart.render();
148
+ if (this.syncGroup) {
149
+ this.chart.group = this.syncGroup;
150
+ echarts.connect(this.syncGroup);
177
151
  }
178
152
 
153
+ let firstResize = true;
154
+ this.resizeObserver = new ResizeObserver(() => {
155
+ if (firstResize) { firstResize = false; return; }
156
+ this.chart?.resize();
157
+ });
158
+ this.resizeObserver.observe(host);
179
159
 
180
- super.firstUpdated(_changedProperties);
160
+ if (this.live && this.dataUrl) this.startLive();
161
+ }
162
+
163
+ private startLive() {
164
+ this.liveTimer = window.setInterval(async () => {
165
+ try {
166
+ const res = await fetch(this.dataUrl);
167
+ const newData = await res.json() as SeriesItem[];
168
+ this.data = newData;
169
+ this.chart?.setOption({ ...this.buildOption() });
170
+ } catch {
171
+ /* swallow transient fetch errors */
172
+ }
173
+ }, this.liveInterval);
174
+ }
175
+
176
+ private reinit() {
177
+ this.chart?.dispose();
178
+ this.chart = undefined;
179
+ this.initChart();
181
180
  }
182
181
 
183
182
  attributeChangedCallback(name: string, _old: string | null, value: string | null) {
184
- if (this.chart && name === 't') {
185
- this.chart.updateOptions({
186
- // colors: value === 'dark' ? this.darkColors : this.lightColors
187
- });
183
+ super.attributeChangedCallback(name, _old, value);
184
+ if (!this.chart) return;
185
+
186
+ if (name === 't') { this.reinit(); return; }
187
+ if (name === 'type') { this.reinit(); return; }
188
+ if (name === 'height') {
189
+ const host = this.shadowRoot?.getElementById('chart') as HTMLElement | null;
190
+ if (host) host.style.height = `${this.height}px`;
191
+ this.chart.resize();
192
+ return;
188
193
  }
189
-
190
- if (this.chart && name === 'height') {
191
- this.chart.updateOptions({
192
- chart: {
193
- height: value + 'px'
194
- }
195
- });
194
+ if (name === 'sync-group') {
195
+ if (value) {
196
+ this.chart.group = value;
197
+ echarts.connect(value);
198
+ } else {
199
+ this.chart.group = '';
200
+ }
201
+ return;
196
202
  }
203
+ this.chart.setOption({ ...this.buildOption() });
204
+ }
197
205
 
198
- super.attributeChangedCallback(name, _old, value);
206
+ disconnectedCallback() {
207
+ super.disconnectedCallback();
208
+ if (this.liveTimer) clearInterval(this.liveTimer);
209
+ this.resizeObserver?.disconnect();
210
+ this.chart?.dispose();
211
+ this.chart = undefined;
199
212
  }
200
213
 
201
214
  protected render(): unknown {
202
- return html`
203
- <div class="chart">
204
- <div id="chart"></div>
205
- </div>
206
- `;
215
+ return html`<div class="chart"><div id="chart"></div></div>`;
207
216
  }
208
- }
217
+ }
@@ -6,6 +6,5 @@
6
6
  }
7
7
 
8
8
  .chart {
9
- overflow: hidden;
10
9
  position: relative;
11
10
  }
@@ -1,10 +1,54 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any */
1
2
  import '../../../dist/zn.min.js';
2
3
  import { expect, fixture, html } from '@open-wc/testing';
3
4
 
4
5
  describe('<zn-chart>', () => {
5
- it('should render a component', async () => {
6
- const el = await fixture(html` <zn-chart></zn-chart> `);
7
-
6
+ it('renders a component', async () => {
7
+ const el = await fixture(html`<zn-chart></zn-chart>`);
8
8
  expect(el).to.exist;
9
9
  });
10
- });
10
+
11
+ it('renders a bar chart with data and categories', async () => {
12
+ const el: any = await fixture(html`
13
+ <zn-chart
14
+ type="bar"
15
+ .data=${[{ name: 'S', data: [1, 2, 3] }]}
16
+ .categories=${['A', 'B', 'C']}
17
+ ></zn-chart>
18
+ `);
19
+ await el.updateComplete;
20
+ await new Promise((r) => setTimeout(r, 50));
21
+ const canvas = el.shadowRoot.querySelector('canvas');
22
+ expect(canvas).to.exist;
23
+ });
24
+
25
+ it('renders a sankey chart', async () => {
26
+ const el: any = await fixture(html`
27
+ <zn-chart
28
+ type="sankey"
29
+ .data=${[{
30
+ name: 'Flow',
31
+ data: [
32
+ { source: 'A', target: 'B', value: 10 },
33
+ { source: 'B', target: 'C', value: 5 },
34
+ ],
35
+ }]}
36
+ ></zn-chart>
37
+ `);
38
+ await el.updateComplete;
39
+ await new Promise((r) => setTimeout(r, 50));
40
+ const canvas = el.shadowRoot.querySelector('canvas');
41
+ expect(canvas).to.exist;
42
+ });
43
+
44
+ it('joins a sync-group when the attribute is set', async () => {
45
+ const a: any = await fixture(html`
46
+ <zn-chart sync-group="g1" type="bar"
47
+ .data=${[{ name: 'S', data: [1, 2] }]}
48
+ .categories=${['A', 'B']}></zn-chart>
49
+ `);
50
+ await a.updateComplete;
51
+ await new Promise((r) => setTimeout(r, 50));
52
+ expect(a.chart?.group).to.equal('g1');
53
+ });
54
+ });
@@ -2,7 +2,8 @@ import './context-menu-component';
2
2
  import {html} from "lit";
3
3
  import {litToHTML} from "../../../../utilities/lit-to-html";
4
4
  import {type ResultItem} from "./context-menu-component";
5
- import Quill, {Delta} from "quill";
5
+ import Quill from "quill";
6
+ import Delta from "quill-delta";
6
7
  import ZnEditorQuickAction from "./quick-action";
7
8
  import type {EditorFeatureConfig} from "../../editor.component";
8
9
  import type ContextMenuComponent from "./context-menu-component";
@@ -5,9 +5,10 @@ import {FormControlController, validValidityState} from "../../internal/form";
5
5
  import {property, query, state} from 'lit/decorators.js';
6
6
  import {styleMap} from 'lit/directives/style-map.js';
7
7
  import {unsafeHTML} from "lit/directives/unsafe-html.js";
8
- import type {ZincFormControl} from '../../internal/zinc-element';
9
8
  import ZincElement from '../../internal/zinc-element';
10
9
 
10
+ import type {ZincFormControl} from '../../internal/zinc-element';
11
+
11
12
  import styles from './rating.scss';
12
13
 
13
14
  /**