lupine.components 1.1.55 → 1.1.56

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 (136) hide show
  1. package/LICENSE +21 -21
  2. package/package.json +1 -1
  3. package/src/component-pool/aspect-ratio/aspect-ratio-demo.tsx +103 -103
  4. package/src/component-pool/avatar/avatar-demo.tsx +127 -127
  5. package/src/component-pool/badge/badge-demo.tsx +152 -152
  6. package/src/component-pool/breadcrumbs/breadcrumbs-demo.tsx +105 -105
  7. package/src/component-pool/card/card-demo.tsx +179 -179
  8. package/src/component-pool/carousel/carousel-demo.tsx +191 -191
  9. package/src/component-pool/cascader/cascader-demo.tsx +127 -127
  10. package/src/component-pool/charts/area-chart-demo.tsx +106 -106
  11. package/src/component-pool/charts/area-chart.tsx +254 -254
  12. package/src/component-pool/charts/bar-chart-demo.tsx +107 -107
  13. package/src/component-pool/charts/bar-chart.tsx +223 -223
  14. package/src/component-pool/charts/chart-utils.ts +182 -182
  15. package/src/component-pool/charts/column-chart-demo.tsx +111 -111
  16. package/src/component-pool/charts/column-chart.tsx +230 -230
  17. package/src/component-pool/charts/donut-chart-demo.tsx +112 -112
  18. package/src/component-pool/charts/donut-chart.tsx +6 -6
  19. package/src/component-pool/charts/gauge-chart-demo.tsx +101 -101
  20. package/src/component-pool/charts/gauge-chart.tsx +117 -117
  21. package/src/component-pool/charts/index.ts +19 -19
  22. package/src/component-pool/charts/line-chart-demo.tsx +109 -109
  23. package/src/component-pool/charts/line-chart.tsx +233 -233
  24. package/src/component-pool/charts/pie-chart-demo.tsx +133 -133
  25. package/src/component-pool/charts/pie-chart.tsx +142 -142
  26. package/src/component-pool/charts/radar-chart-demo.tsx +88 -88
  27. package/src/component-pool/charts/radar-chart.tsx +231 -231
  28. package/src/component-pool/charts/scatter-chart-demo.tsx +88 -88
  29. package/src/component-pool/charts/scatter-chart.tsx +237 -237
  30. package/src/component-pool/copy-button/copy-button-demo.tsx +71 -71
  31. package/src/component-pool/date-picker/date-picker-demo.tsx +84 -84
  32. package/src/component-pool/date-picker/date-picker.tsx +437 -437
  33. package/src/component-pool/date-picker/index.ts +2 -2
  34. package/src/component-pool/floating-icon-menu/floating-icon-menu-demo.tsx +94 -94
  35. package/src/component-pool/floating-icon-menu/floating-icon-menu.tsx +241 -241
  36. package/src/component-pool/floating-icon-menu/index.ts +2 -2
  37. package/src/component-pool/h-editor/h-editor-demo.tsx +112 -112
  38. package/src/component-pool/h-editor/h-editor.ts +862 -862
  39. package/src/component-pool/h-editor/index.ts +2 -2
  40. package/src/component-pool/i-editor/i-editor-demo.tsx +72 -72
  41. package/src/component-pool/i-editor/i-editor-frames.ts +42 -42
  42. package/src/component-pool/i-editor/i-editor-styles.ts +22 -22
  43. package/src/component-pool/i-editor/i-editor.ts +3869 -3869
  44. package/src/component-pool/i-editor/index.ts +2 -2
  45. package/src/component-pool/map-wrapper/map-wrapper-demo.tsx +55 -55
  46. package/src/component-pool/p-editor/index.ts +4 -4
  47. package/src/component-pool/p-editor/p-editor-demo.tsx +51 -51
  48. package/src/component-pool/p-editor/p-editor-styles.ts +44 -44
  49. package/src/component-pool/p-editor/p-editor-types.ts +4 -4
  50. package/src/component-pool/p-editor/p-editor-utils.ts +38 -38
  51. package/src/component-pool/p-editor/p-editor.ts +2477 -2477
  52. package/src/component-pool/pdf-viewer/index.ts +2 -2
  53. package/src/component-pool/pdf-viewer/pdf-viewer-demo.tsx +116 -116
  54. package/src/component-pool/pdf-viewer/pdf-viewer.tsx +56 -56
  55. package/src/component-pool/picker-helper.tsx +222 -222
  56. package/src/component-pool/pull-to-refresh/pull-to-refresh-demo.tsx +99 -99
  57. package/src/component-pool/qrcode/index.ts +3 -3
  58. package/src/component-pool/qrcode/qrcode-demo.tsx +102 -102
  59. package/src/component-pool/qrcode/qrcode.tsx +169 -169
  60. package/src/component-pool/radial-progress/index.ts +2 -2
  61. package/src/component-pool/radial-progress/radial-progress-demo.tsx +84 -84
  62. package/src/component-pool/radial-progress/radial-progress.tsx +118 -118
  63. package/src/component-pool/range/gauge-demo.tsx +113 -113
  64. package/src/component-pool/range/gauge.tsx +392 -392
  65. package/src/component-pool/range/range-demo.tsx +97 -97
  66. package/src/component-pool/search-input/index.ts +2 -2
  67. package/src/component-pool/search-input/search-input-demo.tsx +50 -50
  68. package/src/component-pool/search-input/search-input.tsx +125 -125
  69. package/src/component-pool/skeleton/skeleton-card.tsx +114 -114
  70. package/src/component-pool/skeleton/skeleton-demo.tsx +55 -55
  71. package/src/component-pool/svg-icons.ts +95 -95
  72. package/src/component-pool/svg-props.tsx +144 -144
  73. package/src/component-pool/tag-input/index.ts +2 -2
  74. package/src/component-pool/tag-input/tag-input-demo.tsx +47 -47
  75. package/src/component-pool/tag-input/tag-input.tsx +166 -166
  76. package/src/component-pool/time-picker/index.ts +2 -2
  77. package/src/component-pool/time-picker/time-picker-demo.tsx +80 -80
  78. package/src/component-pool/time-picker/time-picker.tsx +311 -311
  79. package/src/component-pool/timeline/timeline-demo.tsx +75 -75
  80. package/src/component-pool/tooltip/tooltip-demo.tsx +144 -144
  81. package/src/component-pool/tour/tour-demo.tsx +92 -92
  82. package/src/component-pool/youtube-player/index.ts +2 -2
  83. package/src/component-pool/youtube-player/youtube-player-demo.tsx +57 -57
  84. package/src/component-pool/youtube-player/youtube-player.tsx +116 -116
  85. package/src/components/action-sheet-color.tsx +269 -269
  86. package/src/components/action-sheet-date.tsx +299 -299
  87. package/src/components/action-sheet-demo.tsx +216 -216
  88. package/src/components/action-sheet-theme.tsx +150 -150
  89. package/src/components/action-sheet-time.tsx +157 -157
  90. package/src/components/input-number.tsx +206 -206
  91. package/src/components/input-with-title-demo.tsx +42 -42
  92. package/src/components/loading-spin.tsx +74 -74
  93. package/src/components/message-box-demo.tsx +107 -107
  94. package/src/components/mobile-components/mobile-side-menu-demo.tsx +47 -47
  95. package/src/components/modal-demo.tsx +56 -56
  96. package/src/components/notice-message-demo.tsx +57 -57
  97. package/src/components/paging-link-demo.tsx +430 -430
  98. package/src/components/popup-menu-demo.tsx +172 -172
  99. package/src/components/progress-demo.tsx +53 -53
  100. package/src/components/radio-label-demo.tsx +41 -41
  101. package/src/components/redirect-demo.tsx +36 -36
  102. package/src/components/resizable-splitter-demo.tsx +104 -104
  103. package/src/components/select-angle-demo.tsx +32 -32
  104. package/src/components/select-with-title-demo.tsx +53 -53
  105. package/src/components/slide-tab-component-demo.tsx +86 -86
  106. package/src/components/spinner-demo.tsx +52 -52
  107. package/src/components/stars-component-demo.tsx +36 -36
  108. package/src/components/switch-option-component.tsx +74 -74
  109. package/src/components/switch-option-demo.tsx +36 -36
  110. package/src/components/tabs-demo.tsx +42 -42
  111. package/src/components/text-glow-demo.tsx +46 -46
  112. package/src/components/text-scale-demo.tsx +41 -41
  113. package/src/components/text-wave-demo.tsx +48 -48
  114. package/src/components/toggle-button-demo.tsx +230 -230
  115. package/src/demo/demo-page.tsx +271 -271
  116. package/src/demo/demo-registry.ts +143 -143
  117. package/src/demo/demo-render-page.tsx +94 -94
  118. package/src/demo/demo-types.ts +22 -22
  119. package/src/demo/mock/demo-icons.ts +72 -72
  120. package/src/demo/mock/responsive-demo-mock-pages.tsx +197 -197
  121. package/src/demo/mock/side-menu-mock.tsx +113 -113
  122. package/src/demo/mock/user-settings-mock.tsx +310 -310
  123. package/src/frames/responsive-frame-demo.tsx +120 -120
  124. package/src/frames/slider-frame-demo.tsx +111 -111
  125. package/src/frames/slider-helper.tsx +194 -194
  126. package/src/lib/button-async-lock.tsx +43 -43
  127. package/src/lib/canvas/canvas-drawing.ts +503 -503
  128. package/src/lib/canvas/canvas-geometry.ts +255 -255
  129. package/src/lib/canvas/canvas-helper.tsx +176 -176
  130. package/src/lib/canvas/canvas-image.ts +110 -110
  131. package/src/lib/canvas/canvas-types.ts +93 -93
  132. package/src/lib/canvas/index.ts +5 -5
  133. package/src/lib/disable-console.ts +11 -11
  134. package/src/lib/disable-debug.ts +8 -8
  135. package/src/lib/support-safe-area.ts +25 -25
  136. package/tsconfig.json +113 -113
@@ -1,107 +1,107 @@
1
- import { BarChart } from './bar-chart';
2
- import { DemoStory } from '../../demo/demo-types';
3
- import { ChartData } from './chart-utils';
4
-
5
- const singleSeriesData: ChartData = {
6
- labels: ['United States', 'China', 'Japan', 'Germany', 'United Kingdom'],
7
- series: [
8
- {
9
- name: 'GDP (Trillions)',
10
- data: [25.4, 17.9, 4.2, 4.0, 3.0],
11
- },
12
- ],
13
- };
14
-
15
- const multiSeriesData: ChartData = {
16
- labels: ['Q1', 'Q2', 'Q3', 'Q4'],
17
- series: [
18
- { name: 'Product A', data: [40, 30, 50, 70] },
19
- { name: 'Product B', data: [20, 40, 30, 60] },
20
- ],
21
- };
22
-
23
- export const barChartDemo: DemoStory<any> = {
24
- id: 'barChartDemo',
25
- text: 'Bar Chart',
26
- args: {
27
- title: 'Top Economies',
28
- width: '100%',
29
- showLegend: true,
30
- },
31
- argTypes: {
32
- title: { control: 'text' },
33
- width: { control: 'text' },
34
- showLegend: { control: 'boolean' },
35
- },
36
- render: (args: any) => {
37
- const css = {
38
- display: 'flex',
39
- flexDirection: 'column',
40
- gap: '48px',
41
- padding: '24px',
42
- alignItems: 'center',
43
-
44
- '.demo-section': {
45
- display: 'flex',
46
- flexDirection: 'column',
47
- alignItems: 'center',
48
- gap: '24px',
49
- width: '100%',
50
- maxWidth: '800px',
51
- },
52
- '.section-title': {
53
- fontSize: '20px',
54
- fontWeight: 'bold',
55
- color: 'var(--primary-color)',
56
- marginBottom: '16px',
57
- borderBottom: '2px solid var(--secondary-border-color)',
58
- width: '100%',
59
- paddingBottom: '8px',
60
- },
61
- '.chart-box': {
62
- padding: '20px',
63
- border: '1px solid var(--secondary-border-color)',
64
- borderRadius: '8px',
65
- backgroundColor: 'var(--primary-bg-color)',
66
- width: '100%',
67
- },
68
- };
69
-
70
- return (
71
- <div css={css}>
72
- <section class='demo-section'>
73
- <div class='section-title'>Interactive Control</div>
74
- <div class='chart-box'>
75
- <BarChart
76
- data={singleSeriesData}
77
- title={args.title}
78
- width={args.width}
79
- showLegend={args.showLegend}
80
- xAxisFormatter={(val) => '$' + val + 'T'}
81
- />
82
- </div>
83
- </section>
84
-
85
- <section class='demo-section'>
86
- <div class='section-title'>Multi-Series (Grouped Horizontal)</div>
87
- <div class='chart-box'>
88
- <BarChart data={multiSeriesData} title='Quarterly Comparison' showLegend={true} />
89
- </div>
90
- </section>
91
- </div>
92
- );
93
- },
94
- code: `import { BarChart } from 'lupine.components/component-pool/charts';
95
-
96
- const data = {
97
- labels: ['US', 'CN', 'JP'],
98
- series: [{ name: 'GDP', data: [25, 18, 4] }]
99
- };
100
-
101
- <BarChart
102
- data={data}
103
- title="Top Economies"
104
- xAxisFormatter={val => '$' + val + 'T'}
105
- />
106
- `,
107
- };
1
+ import { BarChart } from './bar-chart';
2
+ import { DemoStory } from '../../demo/demo-types';
3
+ import { ChartData } from './chart-utils';
4
+
5
+ const singleSeriesData: ChartData = {
6
+ labels: ['United States', 'China', 'Japan', 'Germany', 'United Kingdom'],
7
+ series: [
8
+ {
9
+ name: 'GDP (Trillions)',
10
+ data: [25.4, 17.9, 4.2, 4.0, 3.0],
11
+ },
12
+ ],
13
+ };
14
+
15
+ const multiSeriesData: ChartData = {
16
+ labels: ['Q1', 'Q2', 'Q3', 'Q4'],
17
+ series: [
18
+ { name: 'Product A', data: [40, 30, 50, 70] },
19
+ { name: 'Product B', data: [20, 40, 30, 60] },
20
+ ],
21
+ };
22
+
23
+ export const barChartDemo: DemoStory<any> = {
24
+ id: 'barChartDemo',
25
+ text: 'Bar Chart',
26
+ args: {
27
+ title: 'Top Economies',
28
+ width: '100%',
29
+ showLegend: true,
30
+ },
31
+ argTypes: {
32
+ title: { control: 'text' },
33
+ width: { control: 'text' },
34
+ showLegend: { control: 'boolean' },
35
+ },
36
+ render: (args: any) => {
37
+ const css = {
38
+ display: 'flex',
39
+ flexDirection: 'column',
40
+ gap: '48px',
41
+ padding: '24px',
42
+ alignItems: 'center',
43
+
44
+ '.demo-section': {
45
+ display: 'flex',
46
+ flexDirection: 'column',
47
+ alignItems: 'center',
48
+ gap: '24px',
49
+ width: '100%',
50
+ maxWidth: '800px',
51
+ },
52
+ '.section-title': {
53
+ fontSize: '20px',
54
+ fontWeight: 'bold',
55
+ color: 'var(--primary-color)',
56
+ marginBottom: '16px',
57
+ borderBottom: '2px solid var(--secondary-border-color)',
58
+ width: '100%',
59
+ paddingBottom: '8px',
60
+ },
61
+ '.chart-box': {
62
+ padding: '20px',
63
+ border: '1px solid var(--secondary-border-color)',
64
+ borderRadius: '8px',
65
+ backgroundColor: 'var(--primary-bg-color)',
66
+ width: '100%',
67
+ },
68
+ };
69
+
70
+ return (
71
+ <div css={css}>
72
+ <section class='demo-section'>
73
+ <div class='section-title'>Interactive Control</div>
74
+ <div class='chart-box'>
75
+ <BarChart
76
+ data={singleSeriesData}
77
+ title={args.title}
78
+ width={args.width}
79
+ showLegend={args.showLegend}
80
+ xAxisFormatter={(val) => '$' + val + 'T'}
81
+ />
82
+ </div>
83
+ </section>
84
+
85
+ <section class='demo-section'>
86
+ <div class='section-title'>Multi-Series (Grouped Horizontal)</div>
87
+ <div class='chart-box'>
88
+ <BarChart data={multiSeriesData} title='Quarterly Comparison' showLegend={true} />
89
+ </div>
90
+ </section>
91
+ </div>
92
+ );
93
+ },
94
+ code: `import { BarChart } from 'lupine.components/component-pool/charts';
95
+
96
+ const data = {
97
+ labels: ['US', 'CN', 'JP'],
98
+ series: [{ name: 'GDP', data: [25, 18, 4] }]
99
+ };
100
+
101
+ <BarChart
102
+ data={data}
103
+ title="Top Economies"
104
+ xAxisFormatter={val => '$' + val + 'T'}
105
+ />
106
+ `,
107
+ };
@@ -1,223 +1,223 @@
1
- import { bindGlobalStyle, getGlobalStylesId, HtmlVar } from 'lupine.components';
2
- import { chartCommonCss, getChartColor, BasicChartProps } from './chart-utils';
3
- import { Tooltip } from '../tooltip';
4
-
5
- export type BarChartProps = BasicChartProps & {
6
- xAxisFormatter?: (value: number) => string;
7
- };
8
-
9
- export const BarChart = (props: BarChartProps) => {
10
- const globalCssId = getGlobalStylesId(chartCommonCss);
11
- bindGlobalStyle(globalCssId, chartCommonCss);
12
-
13
- const showLegend = props.showLegend !== false;
14
- const series = props.data.series;
15
- const labels = props.data.labels;
16
-
17
- if (!series || series.length === 0 || labels.length === 0) {
18
- return <div class='&-container'>No data</div>;
19
- }
20
-
21
- // Find max value for X axis scale
22
- let maxVal = 0;
23
- series.forEach((s) => {
24
- s.data.forEach((val) => {
25
- if (val > maxVal) maxVal = val;
26
- });
27
- });
28
-
29
- const tickCount = 5;
30
- const order = Math.floor(Math.log10(maxVal || 1));
31
- const magnitude = Math.pow(10, order);
32
- const niceStep = Math.ceil(maxVal / magnitude / tickCount) * magnitude;
33
- const niceMax = niceStep * tickCount;
34
-
35
- const xTicks = Array.from({ length: tickCount + 1 }, (_, i) => parseFloat((i * niceStep).toPrecision(12)));
36
-
37
- const renderChart = (viewBoxWidth: number, viewBoxHeight: number) => {
38
- // Layout parameters for Bar Chart (flipped)
39
- const padding = { top: 20, right: 60, bottom: 40, left: 150 }; // larger left padding for labels
40
-
41
- const chartWidth = viewBoxWidth - padding.left - padding.right;
42
- const chartHeight = viewBoxHeight - padding.top - padding.bottom;
43
-
44
- const groupHeight = chartHeight / Math.max(1, labels.length);
45
- // Reduce barPadding a bit, ensure the whole group fits inside the allocated groupHeight height.
46
- const barPadding = 0.2 * groupHeight;
47
- const barHeight = (groupHeight - barPadding) / Math.max(1, series.length);
48
-
49
- const formatX = props.xAxisFormatter || ((val) => val.toString());
50
-
51
- // Render X Axis Ticks & Grid (Vertical lines now)
52
- const renderXAxis = () => {
53
- return xTicks.map((val) => {
54
- const x = padding.left + (val / niceMax) * chartWidth;
55
- return (
56
- <g>
57
- <line
58
- x1={x}
59
- y1={padding.top}
60
- x2={x}
61
- y2={viewBoxHeight - padding.bottom}
62
- stroke='var(--secondary-border-color)'
63
- stroke-width='1'
64
- stroke-dasharray='4 4'
65
- />
66
- <text
67
- x={x}
68
- y={viewBoxHeight - padding.bottom + 25}
69
- fill='var(--primary-color)'
70
- fontSize='12'
71
- text-anchor='middle'
72
- >
73
- {formatX(val)}
74
- </text>
75
- </g>
76
- );
77
- });
78
- };
79
-
80
- // Render Y Axis Labels (Horizontal text now)
81
- const renderYAxis = () => {
82
- return labels.map((label, index) => {
83
- const y = padding.top + index * groupHeight + groupHeight / 2;
84
- return (
85
- <text
86
- x={padding.left - 15}
87
- y={y + 4} // slight alignment offset
88
- fill='var(--primary-color)'
89
- fontSize='12'
90
- text-anchor='end'
91
- >
92
- {label}
93
- </text>
94
- );
95
- });
96
- };
97
-
98
- // Render Bars (Horizontal rectangles now)
99
- const renderBars = () => {
100
- const bars: any[] = [];
101
-
102
- labels.forEach((label, lIndex) => {
103
- const groupY = padding.top + lIndex * groupHeight + barPadding / 2;
104
-
105
- series.forEach((s, sIndex) => {
106
- const val = s.data[lIndex] || 0;
107
- const color = s.color || getChartColor(sIndex);
108
-
109
- const barWidth = (val / niceMax) * chartWidth;
110
- const x = padding.left;
111
- const y = groupY + sIndex * barHeight;
112
-
113
- const handleMouseEnter = (e: any) => {
114
- Tooltip.show(
115
- e,
116
- <div>
117
- <div style={{ fontWeight: 'bold' }}>{label}</div>
118
- <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
119
- <div style={{ width: '10px', height: '10px', backgroundColor: color, borderRadius: '2px' }} />
120
- <span>
121
- {s.name}: {formatX(val)}
122
- </span>
123
- </div>
124
- </div>,
125
- { position: 'auto' }
126
- );
127
- e.target.style.opacity = 0.8;
128
- };
129
-
130
- const handleMouseLeave = (e: any) => {
131
- Tooltip.hide();
132
- e.target.style.opacity = 1;
133
- };
134
-
135
- bars.push(
136
- <rect
137
- class='chart-element'
138
- x={x}
139
- y={y}
140
- width={barWidth}
141
- height={barHeight - 2} // -2 for slight individual bar padding
142
- fill={color}
143
- rx='2'
144
- ry='2'
145
- onMouseEnter={handleMouseEnter}
146
- onMouseLeave={handleMouseLeave}
147
- />
148
- );
149
- });
150
- });
151
-
152
- return bars;
153
- };
154
-
155
- return (
156
- <svg
157
- class='chart-svg'
158
- style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', overflow: 'visible' }}
159
- viewBox={`0 0 ${viewBoxWidth} ${viewBoxHeight}`}
160
- preserveAspectRatio='none'
161
- >
162
- {/* Base Axis line (Vertical now) */}
163
- <line
164
- x1={padding.left}
165
- y1={padding.top}
166
- x2={padding.left}
167
- y2={viewBoxHeight - padding.bottom}
168
- stroke='var(--secondary-border-color)'
169
- stroke-width='2'
170
- />
171
-
172
- <g class='x-axis'>{renderXAxis()}</g>
173
- <g class='y-axis'>{renderYAxis()}</g>
174
- <g class='bars'>{renderBars()}</g>
175
- </svg>
176
- );
177
- };
178
-
179
- const chartVar = new HtmlVar(renderChart(1000, Math.max(300, labels.length * 40 + 60)));
180
-
181
- const ref = {
182
- globalCssId,
183
- onLoad: async (el: Element) => {
184
- const ro = new ResizeObserver((entries) => {
185
- const { width } = entries[0].contentRect;
186
- if (width > 50) {
187
- chartVar.value = renderChart(width, Math.max(300, labels.length * 40 + 60));
188
- }
189
- });
190
- ro.observe(el);
191
- (el as any)._ro = ro;
192
- },
193
- onUnload: async (el: Element) => {
194
- if ((el as any)._ro) {
195
- (el as any)._ro.disconnect();
196
- }
197
- },
198
- };
199
-
200
- const ratio = props.aspectRatio ?? 16 / 9;
201
- const paddingTop = `${(1 / ratio) * 100}%`;
202
-
203
- const styleStr = `width: ${props.width || '100%'};`;
204
-
205
- return (
206
- <div ref={ref} class='&-container' style={styleStr}>
207
- {props.title && <div class='chart-title'>{props.title}</div>}
208
-
209
- <div style={{ flex: 1, position: 'relative', minHeight: 0, paddingTop }}>{chartVar.node}</div>
210
-
211
- {showLegend && (
212
- <div class='chart-legend'>
213
- {series.map((s, i) => (
214
- <div class='legend-item'>
215
- <div class='legend-color' style={{ backgroundColor: s.color || getChartColor(i) }} />
216
- <div>{s.name}</div>
217
- </div>
218
- ))}
219
- </div>
220
- )}
221
- </div>
222
- );
223
- };
1
+ import { bindGlobalStyle, getGlobalStylesId, HtmlVar } from 'lupine.components';
2
+ import { chartCommonCss, getChartColor, BasicChartProps } from './chart-utils';
3
+ import { Tooltip } from '../tooltip';
4
+
5
+ export type BarChartProps = BasicChartProps & {
6
+ xAxisFormatter?: (value: number) => string;
7
+ };
8
+
9
+ export const BarChart = (props: BarChartProps) => {
10
+ const globalCssId = getGlobalStylesId(chartCommonCss);
11
+ bindGlobalStyle(globalCssId, chartCommonCss);
12
+
13
+ const showLegend = props.showLegend !== false;
14
+ const series = props.data.series;
15
+ const labels = props.data.labels;
16
+
17
+ if (!series || series.length === 0 || labels.length === 0) {
18
+ return <div class='&-container'>No data</div>;
19
+ }
20
+
21
+ // Find max value for X axis scale
22
+ let maxVal = 0;
23
+ series.forEach((s) => {
24
+ s.data.forEach((val) => {
25
+ if (val > maxVal) maxVal = val;
26
+ });
27
+ });
28
+
29
+ const tickCount = 5;
30
+ const order = Math.floor(Math.log10(maxVal || 1));
31
+ const magnitude = Math.pow(10, order);
32
+ const niceStep = Math.ceil(maxVal / magnitude / tickCount) * magnitude;
33
+ const niceMax = niceStep * tickCount;
34
+
35
+ const xTicks = Array.from({ length: tickCount + 1 }, (_, i) => parseFloat((i * niceStep).toPrecision(12)));
36
+
37
+ const renderChart = (viewBoxWidth: number, viewBoxHeight: number) => {
38
+ // Layout parameters for Bar Chart (flipped)
39
+ const padding = { top: 20, right: 60, bottom: 40, left: 150 }; // larger left padding for labels
40
+
41
+ const chartWidth = viewBoxWidth - padding.left - padding.right;
42
+ const chartHeight = viewBoxHeight - padding.top - padding.bottom;
43
+
44
+ const groupHeight = chartHeight / Math.max(1, labels.length);
45
+ // Reduce barPadding a bit, ensure the whole group fits inside the allocated groupHeight height.
46
+ const barPadding = 0.2 * groupHeight;
47
+ const barHeight = (groupHeight - barPadding) / Math.max(1, series.length);
48
+
49
+ const formatX = props.xAxisFormatter || ((val) => val.toString());
50
+
51
+ // Render X Axis Ticks & Grid (Vertical lines now)
52
+ const renderXAxis = () => {
53
+ return xTicks.map((val) => {
54
+ const x = padding.left + (val / niceMax) * chartWidth;
55
+ return (
56
+ <g>
57
+ <line
58
+ x1={x}
59
+ y1={padding.top}
60
+ x2={x}
61
+ y2={viewBoxHeight - padding.bottom}
62
+ stroke='var(--secondary-border-color)'
63
+ stroke-width='1'
64
+ stroke-dasharray='4 4'
65
+ />
66
+ <text
67
+ x={x}
68
+ y={viewBoxHeight - padding.bottom + 25}
69
+ fill='var(--primary-color)'
70
+ fontSize='12'
71
+ text-anchor='middle'
72
+ >
73
+ {formatX(val)}
74
+ </text>
75
+ </g>
76
+ );
77
+ });
78
+ };
79
+
80
+ // Render Y Axis Labels (Horizontal text now)
81
+ const renderYAxis = () => {
82
+ return labels.map((label, index) => {
83
+ const y = padding.top + index * groupHeight + groupHeight / 2;
84
+ return (
85
+ <text
86
+ x={padding.left - 15}
87
+ y={y + 4} // slight alignment offset
88
+ fill='var(--primary-color)'
89
+ fontSize='12'
90
+ text-anchor='end'
91
+ >
92
+ {label}
93
+ </text>
94
+ );
95
+ });
96
+ };
97
+
98
+ // Render Bars (Horizontal rectangles now)
99
+ const renderBars = () => {
100
+ const bars: any[] = [];
101
+
102
+ labels.forEach((label, lIndex) => {
103
+ const groupY = padding.top + lIndex * groupHeight + barPadding / 2;
104
+
105
+ series.forEach((s, sIndex) => {
106
+ const val = s.data[lIndex] || 0;
107
+ const color = s.color || getChartColor(sIndex);
108
+
109
+ const barWidth = (val / niceMax) * chartWidth;
110
+ const x = padding.left;
111
+ const y = groupY + sIndex * barHeight;
112
+
113
+ const handleMouseEnter = (e: any) => {
114
+ Tooltip.show(
115
+ e,
116
+ <div>
117
+ <div style={{ fontWeight: 'bold' }}>{label}</div>
118
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
119
+ <div style={{ width: '10px', height: '10px', backgroundColor: color, borderRadius: '2px' }} />
120
+ <span>
121
+ {s.name}: {formatX(val)}
122
+ </span>
123
+ </div>
124
+ </div>,
125
+ { position: 'auto' }
126
+ );
127
+ e.target.style.opacity = 0.8;
128
+ };
129
+
130
+ const handleMouseLeave = (e: any) => {
131
+ Tooltip.hide();
132
+ e.target.style.opacity = 1;
133
+ };
134
+
135
+ bars.push(
136
+ <rect
137
+ class='chart-element'
138
+ x={x}
139
+ y={y}
140
+ width={barWidth}
141
+ height={barHeight - 2} // -2 for slight individual bar padding
142
+ fill={color}
143
+ rx='2'
144
+ ry='2'
145
+ onMouseEnter={handleMouseEnter}
146
+ onMouseLeave={handleMouseLeave}
147
+ />
148
+ );
149
+ });
150
+ });
151
+
152
+ return bars;
153
+ };
154
+
155
+ return (
156
+ <svg
157
+ class='chart-svg'
158
+ style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', overflow: 'visible' }}
159
+ viewBox={`0 0 ${viewBoxWidth} ${viewBoxHeight}`}
160
+ preserveAspectRatio='none'
161
+ >
162
+ {/* Base Axis line (Vertical now) */}
163
+ <line
164
+ x1={padding.left}
165
+ y1={padding.top}
166
+ x2={padding.left}
167
+ y2={viewBoxHeight - padding.bottom}
168
+ stroke='var(--secondary-border-color)'
169
+ stroke-width='2'
170
+ />
171
+
172
+ <g class='x-axis'>{renderXAxis()}</g>
173
+ <g class='y-axis'>{renderYAxis()}</g>
174
+ <g class='bars'>{renderBars()}</g>
175
+ </svg>
176
+ );
177
+ };
178
+
179
+ const chartVar = new HtmlVar(renderChart(1000, Math.max(300, labels.length * 40 + 60)));
180
+
181
+ const ref = {
182
+ globalCssId,
183
+ onLoad: async (el: Element) => {
184
+ const ro = new ResizeObserver((entries) => {
185
+ const { width } = entries[0].contentRect;
186
+ if (width > 50) {
187
+ chartVar.value = renderChart(width, Math.max(300, labels.length * 40 + 60));
188
+ }
189
+ });
190
+ ro.observe(el);
191
+ (el as any)._ro = ro;
192
+ },
193
+ onUnload: async (el: Element) => {
194
+ if ((el as any)._ro) {
195
+ (el as any)._ro.disconnect();
196
+ }
197
+ },
198
+ };
199
+
200
+ const ratio = props.aspectRatio ?? 16 / 9;
201
+ const paddingTop = `${(1 / ratio) * 100}%`;
202
+
203
+ const styleStr = `width: ${props.width || '100%'};`;
204
+
205
+ return (
206
+ <div ref={ref} class='&-container' style={styleStr}>
207
+ {props.title && <div class='chart-title'>{props.title}</div>}
208
+
209
+ <div style={{ flex: 1, position: 'relative', minHeight: 0, paddingTop }}>{chartVar.node}</div>
210
+
211
+ {showLegend && (
212
+ <div class='chart-legend'>
213
+ {series.map((s, i) => (
214
+ <div class='legend-item'>
215
+ <div class='legend-color' style={{ backgroundColor: s.color || getChartColor(i) }} />
216
+ <div>{s.name}</div>
217
+ </div>
218
+ ))}
219
+ </div>
220
+ )}
221
+ </div>
222
+ );
223
+ };