Skip to content

Commit bf3497c

Browse files
authored
Merge pull request #1440 from easyops-cn/steve/indictors
Steve/indictors
2 parents fc7b851 + e9b8164 commit bf3497c

File tree

26 files changed

+872
-276
lines changed

26 files changed

+872
-276
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
地球加轨道的数据展示构件。
2+
3+
## Examples
4+
5+
### Basic
6+
7+
```yaml preview height="660px"
8+
brick: div
9+
properties:
10+
style:
11+
height: calc(100vh - 2em)
12+
children:
13+
- brick: data-view.globe-with-orbit-indicator
14+
properties:
15+
centerDataSource:
16+
label: 资产总数
17+
value: 30123
18+
dataSource:
19+
- label: 低值易耗品
20+
value: 3889
21+
- label: 摊销资产
22+
value: 2087
23+
- label: 固定资产
24+
value: 12088
25+
- label: 无形资产
26+
value: 1082
27+
- label: 在建工程
28+
value: 10997
29+
- label: 其他资产
30+
value: 4203
31+
cornerDataSource:
32+
- label: 资产增长
33+
value: 43
34+
color: red
35+
- label: 资产减少
36+
value: 21
37+
color: green
38+
# Currently this brick only works within dark theme
39+
lifeCycle:
40+
onPageLoad:
41+
action: theme.setTheme
42+
args:
43+
- dark-v2
44+
```

bricks/data-view/src/bootstrap.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,4 @@ import "./crystal-ball-indicator/index.js";
4343
import "./globe-with-halo-indicator/index.js";
4444
import "./globe-with-gear-indicator/index.js";
4545
import "./bubbles-indicator/index.js";
46+
import "./globe-with-orbit-indicator/index.js";
-9.5 KB
Loading

bricks/data-view/src/bubbles-indicator/index.tsx

Lines changed: 34 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useMemo, useState } from "react";
1+
import React, { useMemo } from "react";
22
import {
33
forceSimulation,
44
forceCollide,
@@ -11,16 +11,19 @@ import {
1111
type SimulationLinkDatum,
1212
} from "d3-force";
1313
import { createDecorators } from "@next-core/element";
14-
import { ReactNextElement, wrapBrick } from "@next-core/react-element";
14+
import { ReactNextElement } from "@next-core/react-element";
1515
import "@next-core/theme";
16-
import ResizeObserver from "resize-observer-polyfill";
17-
import type { Tag, TagProps } from "@next-bricks/basic/tag";
16+
import { formatValue } from "../shared/formatValue";
17+
import { CornerIndicator } from "../shared/CornerIndicator";
18+
import { useContainerScale } from "../shared/useContainerScale";
19+
import { useCenterScale } from "../shared/useCenterScale";
1820
import "../fonts/ALiBaBaPuHuiTi.css";
1921
import "../fonts/PangMenZhengDaoBiaoTiTi.css";
2022
import styleText from "./styles.shadow.css";
23+
import cornerStyleText from "../shared/CornerIndicator.shadow.css";
2124

22-
const BASE_WIDTH = 800;
23-
const BASE_HEIGHT = 640;
25+
const BASE_WIDTH = 900;
26+
const BASE_HEIGHT = 700;
2427
const CENTER_BUBBLE_RADIUS = 196;
2528
const OTHER_BUBBLE_MAX_RADIUS = 81;
2629
const OTHER_BUBBLE_MIN_RADIUS = 40;
@@ -29,12 +32,6 @@ const RANDOM_BUBBLE_MIN_RADIUS = 10;
2932
const RANDOM_BUBBLE_MAX_RADIUS = 22;
3033
const TOTAL_BUBBLE_COUNT = 18;
3134

32-
const numberFormatter = new Intl.NumberFormat("zh-CN", {
33-
useGrouping: true,
34-
});
35-
36-
const WrappedTag = wrapBrick<Tag, TagProps>("eo-tag");
37-
3835
const { defineElement, property } = createDecorators();
3936

4037
export interface BubblesIndicatorProps {
@@ -47,6 +44,10 @@ export interface BubblesIndicatorProps {
4744
export interface DataItem {
4845
label: string;
4946
value: string | number;
47+
/**
48+
* 用于计算气泡相对大小的数值。
49+
*/
50+
numberValue?: number;
5051
}
5152

5253
export interface CornerDataItem extends DataItem {
@@ -74,7 +75,7 @@ interface NumberedDataItem extends DataItem {
7475
*/
7576
export
7677
@defineElement("data-view.bubbles-indicator", {
77-
styleTexts: [styleText],
78+
styleTexts: [styleText, cornerStyleText],
7879
})
7980
class BubblesIndicator
8081
extends ReactNextElement
@@ -129,32 +130,24 @@ export function BubblesIndicatorComponent({
129130
cornerDataSource,
130131
maxScale,
131132
}: BubblesIndicatorComponentProps) {
132-
const [scale, setScale] = useState<number | null>(null);
133-
134-
useEffect(() => {
135-
// 当容器宽高低于预设值时,图形会自动缩小
136-
const observer = new ResizeObserver((entries) => {
137-
for (const entry of entries) {
138-
if (entry.target === root) {
139-
const { width, height } = entry.contentRect;
140-
// 宽度大于高度,因为有水平方向排列的标签文字
141-
setScale(
142-
Math.min(maxScale ?? 1, width / BASE_WIDTH, height / BASE_HEIGHT)
143-
);
144-
}
145-
}
146-
});
147-
observer.observe(root);
148-
return () => observer.disconnect();
149-
}, [maxScale, root]);
133+
const scale = useContainerScale({
134+
width: BASE_WIDTH,
135+
height: BASE_HEIGHT,
136+
root,
137+
maxScale,
138+
});
139+
const [centerValueScale, centerValueRef] = useCenterScale(280);
150140

151141
// 使用 d3 力学布局计算气泡位置,将普通数据排列在中心数据周围,并填充一些小的气泡
152142
const labels = useMemo(() => {
153143
const numberedDataSource: NumberedDataItem[] =
154144
dataSource?.slice(0, 12)?.map((item) => ({
155145
...item,
156146
positiveNumberValue: Math.abs(
157-
typeof item.value === "number" ? item.value : parseFloat(item.value)
147+
item.numberValue ??
148+
(typeof item.value === "number"
149+
? item.value
150+
: parseFloat(item.value))
158151
),
159152
})) ?? [];
160153
const positiveNumberValues = numberedDataSource.map(
@@ -249,7 +242,14 @@ export function BubblesIndicatorComponent({
249242
<div className="inner-ring"></div>
250243
<div className="center">
251244
<div className="center-label">{centerDataSource?.label}</div>
252-
<div className="center-value">
245+
<div
246+
className="center-value"
247+
ref={centerValueRef}
248+
style={{
249+
visibility: centerValueScale === null ? "hidden" : "visible",
250+
transform: `scale(${centerValueScale ?? 1})`,
251+
}}
252+
>
253253
{formatValue(centerDataSource?.value)}
254254
</div>
255255
</div>
@@ -277,32 +277,11 @@ export function BubblesIndicatorComponent({
277277
))}
278278
</div>
279279
</div>
280-
<div className="corner">
281-
{cornerDataSource?.map((item, index) => (
282-
<div key={index} className="corner-item">
283-
<div className="corner-label">{item.label}</div>
284-
<WrappedTag
285-
className="corner-value"
286-
outline
287-
color={item.color}
288-
tagStyle={{
289-
fontSize: 18,
290-
padding: "2px 16px",
291-
}}
292-
>
293-
{formatValue(item.value)}
294-
</WrappedTag>
295-
</div>
296-
))}
297-
</div>
280+
<CornerIndicator cornerDataSource={cornerDataSource} />
298281
</>
299282
);
300283
}
301284

302-
function formatValue(value: string | number): string {
303-
return typeof value === "number" ? numberFormatter.format(value) : value;
304-
}
305-
306285
function manuallyTickToTheEnd(
307286
simulation: Simulation<ForceNode, SimulationLinkDatum<ForceNode>>
308287
): void {

bricks/data-view/src/bubbles-indicator/styles.shadow.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
background: url(./assets/light.png) no-repeat;
8787
background-size: 338px 478px;
8888
animation: moving-light 3s linear infinite;
89+
pointer-events: none;
8990
}
9091

9192
.bubbles {

bricks/data-view/src/crystal-ball-indicator/index.tsx

Lines changed: 19 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
1-
import React, { useEffect, useMemo, useState } from "react";
1+
import React, { useMemo } from "react";
22
import { createDecorators } from "@next-core/element";
3-
import { ReactNextElement, wrapBrick } from "@next-core/react-element";
3+
import { ReactNextElement } from "@next-core/react-element";
44
import "@next-core/theme";
5-
import ResizeObserver from "resize-observer-polyfill";
6-
import type { Tag, TagProps } from "@next-bricks/basic/tag";
5+
import { formatValue } from "../shared/formatValue";
6+
import { CornerIndicator } from "../shared/CornerIndicator";
7+
import { useContainerScale } from "../shared/useContainerScale";
8+
import { useCenterScale } from "../shared/useCenterScale";
79
import crystalBallVideo from "./assets/crystal-ball.mp4";
810
import "../fonts/ALiBaBaPuHuiTi.css";
911
import "../fonts/PangMenZhengDaoBiaoTiTi.css";
1012
import styleText from "./styles.shadow.css";
13+
import cornerStyleText from "../shared/CornerIndicator.shadow.css";
1114

1215
const RING_SIZE = 572;
1316
const RING_OFFSET = 16;
1417

15-
const numberFormatter = new Intl.NumberFormat("zh-CN", {
16-
useGrouping: true,
17-
});
18-
19-
const WrappedTag = wrapBrick<Tag, TagProps>("eo-tag");
20-
2118
const { defineElement, property } = createDecorators();
2219

2320
export interface CrystalBallIndicatorProps {
@@ -46,7 +43,7 @@ interface DataItemWithPosition extends DataItem {
4643
*/
4744
export
4845
@defineElement("data-view.crystal-ball-indicator", {
49-
styleTexts: [styleText],
46+
styleTexts: [styleText, cornerStyleText],
5047
})
5148
class CrystalBallIndicator
5249
extends ReactNextElement
@@ -102,22 +99,8 @@ export function CrystalBallIndicatorComponent({
10299
cornerDataSource,
103100
maxScale,
104101
}: CrystalBallIndicatorComponentProps) {
105-
const [scale, setScale] = useState<number | null>(null);
106-
107-
useEffect(() => {
108-
// 当容器宽高低于预设值时,图形会自动缩小
109-
const observer = new ResizeObserver((entries) => {
110-
for (const entry of entries) {
111-
if (entry.target === root) {
112-
const { width, height } = entry.contentRect;
113-
// 宽度大于高度,因为有水平方向排列的标签文字
114-
setScale(Math.min(maxScale ?? 1, width / 810, height / 604));
115-
}
116-
}
117-
});
118-
observer.observe(root);
119-
return () => observer.disconnect();
120-
}, [maxScale, root]);
102+
const scale = useContainerScale({ width: 810, height: 604, root, maxScale });
103+
const [centerValueScale, centerValueRef] = useCenterScale(280);
121104

122105
// 计算环上标签的位置
123106
// 1. 将数据分为两组,分别在环的两侧
@@ -204,33 +187,19 @@ export function CrystalBallIndicatorComponent({
204187
</div>
205188
<div className="center">
206189
<div className="center-label">{centerDataSource?.label}</div>
207-
<div className="center-value">
190+
<div
191+
className="center-value"
192+
ref={centerValueRef}
193+
style={{
194+
visibility: centerValueScale === null ? "hidden" : "visible",
195+
transform: `scale(${centerValueScale ?? 1})`,
196+
}}
197+
>
208198
{formatValue(centerDataSource?.value)}
209199
</div>
210200
</div>
211201
</div>
212-
<div className="corner">
213-
{cornerDataSource?.map((item, index) => (
214-
<div key={index} className="corner-item">
215-
<div className="corner-label">{item.label}</div>
216-
<WrappedTag
217-
className="corner-value"
218-
outline
219-
color={item.color}
220-
tagStyle={{
221-
fontSize: 18,
222-
padding: "2px 16px",
223-
}}
224-
>
225-
{formatValue(item.value)}
226-
</WrappedTag>
227-
</div>
228-
))}
229-
</div>
202+
<CornerIndicator cornerDataSource={cornerDataSource} />
230203
</>
231204
);
232205
}
233-
234-
function formatValue(value: string | number): string {
235-
return typeof value === "number" ? numberFormatter.format(value) : value;
236-
}

bricks/data-view/src/crystal-ball-indicator/styles.shadow.css

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
}
8585

8686
.ring-label {
87-
color: #c8e2ff;
87+
color: rgba(200, 226, 255, 0.8);
8888
font-size: 18px;
8989
}
9090

@@ -96,8 +96,11 @@
9696

9797
.center {
9898
position: absolute;
99-
width: 100%;
100-
height: 100%;
99+
top: 50%;
100+
left: 50%;
101+
transform: translate(-50%, -50%);
102+
width: 292px;
103+
height: 292px;
101104
flex-direction: column;
102105
text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.5);
103106
}
@@ -142,28 +145,6 @@
142145
animation: ring-rotate 5s linear infinite;
143146
}
144147

145-
.corner {
146-
position: absolute;
147-
top: 13px;
148-
left: 31px;
149-
font-size: 18px;
150-
}
151-
152-
.corner-item {
153-
display: flex;
154-
margin-bottom: 15px;
155-
}
156-
157-
.corner-label {
158-
font-weight: 500;
159-
line-height: 25px;
160-
}
161-
162-
.corner-value {
163-
display: block;
164-
margin-left: 19px;
165-
}
166-
167148
@keyframes ring-rotate {
168149
from {
169150
transform: rotate(0deg);

0 commit comments

Comments
 (0)