Skip to content

Commit 461148d

Browse files
committed
feat(statistic): add Statistic component
Numeric display with title, prefix/suffix, precision formatting, and custom value styling.
1 parent 2c2f3c6 commit 461148d

10 files changed

Lines changed: 280 additions & 0 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`<Statistic /> should match the snapshot 1`] = `
4+
<DocumentFragment>
5+
<div
6+
class="ty-statistic"
7+
>
8+
<div
9+
class="ty-statistic__title"
10+
>
11+
Active Users
12+
</div>
13+
<div
14+
class="ty-statistic__content"
15+
>
16+
<span
17+
class="ty-statistic__value"
18+
>
19+
112,893
20+
</span>
21+
</div>
22+
</div>
23+
</DocumentFragment>
24+
`;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React from 'react';
2+
import { render } from '@testing-library/react';
3+
import Statistic from '../index';
4+
5+
describe('<Statistic />', () => {
6+
it('should match the snapshot', () => {
7+
const { asFragment } = render(<Statistic title="Active Users" value={112893} />);
8+
expect(asFragment()).toMatchSnapshot();
9+
});
10+
11+
it('should render correctly', () => {
12+
const { container } = render(<Statistic title="Score" value={95} />);
13+
expect(container.firstChild).toHaveClass('ty-statistic');
14+
});
15+
16+
it('should render title', () => {
17+
const { getByText } = render(<Statistic title="Total Sales" value={1000} />);
18+
expect(getByText('Total Sales')).toBeInTheDocument();
19+
});
20+
21+
it('should format value with group separator', () => {
22+
const { getByText } = render(<Statistic value={112893} />);
23+
expect(getByText('112,893')).toBeInTheDocument();
24+
});
25+
26+
it('should format value with precision', () => {
27+
const { getByText } = render(<Statistic value={11.28} precision={2} />);
28+
expect(getByText('11.28')).toBeInTheDocument();
29+
});
30+
31+
it('should render prefix and suffix', () => {
32+
const { getByText } = render(
33+
<Statistic value={100} prefix="$" suffix="USD" />
34+
);
35+
expect(getByText('$')).toBeInTheDocument();
36+
expect(getByText('USD')).toBeInTheDocument();
37+
});
38+
39+
it('should use custom formatter', () => {
40+
const formatter = (val: number | string) => `${val}%`;
41+
const { getByText } = render(<Statistic value={95} formatter={formatter} />);
42+
expect(getByText('95%')).toBeInTheDocument();
43+
});
44+
45+
it('should render string value', () => {
46+
const { getByText } = render(<Statistic value="N/A" />);
47+
expect(getByText('N/A')).toBeInTheDocument();
48+
});
49+
});

components/statistic/demo/basic.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<demo>
2+
3+
### Basic
4+
5+
Display a statistic with title and formatted value.
6+
7+
```jsx live
8+
() => {
9+
return (
10+
<div style={{ display: 'flex', gap: '48px' }}>
11+
<Statistic title="Active Users" value={112893} />
12+
<Statistic title="Account Balance" value={112893} precision={2} prefix="$" />
13+
<Statistic title="Growth Rate" value={93.12} suffix="%" precision={2} />
14+
</div>
15+
);
16+
}
17+
```
18+
19+
</demo>

components/statistic/index.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import Basic from './demo/basic.md'
2+
3+
# Statistic
4+
5+
Display a statistic number with title, supporting formatted values, prefix/suffix, and custom formatting.
6+
7+
## Scenario
8+
9+
Used in dashboards and data-heavy pages to highlight key performance indicators (KPIs).
10+
11+
## Usage
12+
13+
```jsx
14+
import { Statistic } from 'tiny-ui';
15+
```
16+
17+
## Examples
18+
19+
<layout>
20+
<column>
21+
<Basic/>
22+
</column>
23+
</layout>
24+
25+
## API
26+
27+
| Property | Description | Type | Default |
28+
| -------------- | ------------------------------------------------ | ----------------------------------------- | ------- |
29+
| title | title of the statistic | ReactNode | |
30+
| value | the value to display | number \| string | |
31+
| precision | number of decimal places | number | |
32+
| prefix | prefix node of value | ReactNode | |
33+
| suffix | suffix node of value | ReactNode | |
34+
| groupSeparator | thousands separator | string | , |
35+
| valueStyle | custom style for value | CSSProperties | |
36+
| formatter | custom value formatter | (value: number \| string) => ReactNode | |

components/statistic/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import Statistic from './statistic';
2+
3+
export default Statistic;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import Basic from './demo/basic.md'
2+
3+
# Statistic 统计数值
4+
5+
展示统计数值,支持格式化数值、前后缀和自定义格式。
6+
7+
## 使用场景
8+
9+
用于仪表盘和数据密集页面,突出展示关键绩效指标(KPI)。
10+
11+
## 使用方式
12+
13+
```jsx
14+
import { Statistic } from 'tiny-ui';
15+
```
16+
17+
## 代码演示
18+
19+
<layout>
20+
<column>
21+
<Basic/>
22+
</column>
23+
</layout>
24+
25+
## API
26+
27+
| 属性 | 说明 | 类型 | 默认值 |
28+
| -------------- | ------------------------ | ----------------------------------------- | ------- |
29+
| title | 统计标题 | ReactNode | |
30+
| value | 展示的数值 | number \| string | |
31+
| precision | 小数点精度 | number | |
32+
| prefix | 数值前缀 | ReactNode | |
33+
| suffix | 数值后缀 | ReactNode | |
34+
| groupSeparator | 千分位分隔符 | string | , |
35+
| valueStyle | 数值样式 | CSSProperties | |
36+
| formatter | 自定义格式化函数 | (value: number \| string) => ReactNode | |

components/statistic/statistic.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React, { useContext } from 'react';
2+
import classNames from 'classnames';
3+
import { ConfigContext } from '../config-provider/config-context';
4+
import { getPrefixCls } from '../_utils/general';
5+
import { StatisticProps } from './types';
6+
7+
const formatValue = (
8+
value: number | string | undefined,
9+
precision?: number,
10+
groupSeparator?: string
11+
): string => {
12+
if (value === undefined) return '';
13+
if (typeof value === 'string') return value;
14+
15+
let val = precision !== undefined ? value.toFixed(precision) : String(value);
16+
17+
if (groupSeparator) {
18+
const parts = val.split('.');
19+
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, groupSeparator);
20+
val = parts.join('.');
21+
}
22+
23+
return val;
24+
};
25+
26+
const Statistic = React.forwardRef<HTMLDivElement, StatisticProps>((props, ref) => {
27+
const {
28+
title,
29+
value,
30+
precision,
31+
prefix,
32+
suffix,
33+
groupSeparator = ',',
34+
valueStyle,
35+
formatter,
36+
prefixCls: customisedCls,
37+
className,
38+
style,
39+
...otherProps
40+
} = props;
41+
42+
const configContext = useContext(ConfigContext);
43+
const prefixCls = getPrefixCls('statistic', configContext.prefixCls, customisedCls);
44+
const cls = classNames(prefixCls, className);
45+
46+
const renderValue = () => {
47+
if (formatter) {
48+
return formatter(value ?? '');
49+
}
50+
return formatValue(value, precision, groupSeparator);
51+
};
52+
53+
return (
54+
<div {...otherProps} ref={ref} className={cls} style={style}>
55+
{title && <div className={`${prefixCls}__title`}>{title}</div>}
56+
<div className={`${prefixCls}__content`} style={valueStyle}>
57+
{prefix && <span className={`${prefixCls}__prefix`}>{prefix}</span>}
58+
<span className={`${prefixCls}__value`}>{renderValue()}</span>
59+
{suffix && <span className={`${prefixCls}__suffix`}>{suffix}</span>}
60+
</div>
61+
</div>
62+
);
63+
});
64+
65+
Statistic.displayName = 'Statistic';
66+
export default Statistic;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
@import '../../style/variables';
2+
3+
.#{$prefix}-statistic {
4+
&__title {
5+
margin-bottom: 4px;
6+
color: var(--ty-color-text-secondary, #{$gray-600});
7+
font-size: $font-size-sm;
8+
}
9+
10+
&__content {
11+
display: flex;
12+
align-items: baseline;
13+
color: var(--ty-color-text, #{$gray-900});
14+
font-size: 24px;
15+
font-weight: 600;
16+
font-family: $font-family-sans-serif;
17+
}
18+
19+
&__prefix {
20+
margin-right: 4px;
21+
display: inline-flex;
22+
align-items: center;
23+
}
24+
25+
&__suffix {
26+
margin-left: 4px;
27+
font-size: $font-size-base;
28+
display: inline-flex;
29+
align-items: center;
30+
}
31+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import './index.scss';

components/statistic/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React from 'react';
2+
import { BaseProps } from '../_utils/props';
3+
4+
export interface StatisticProps
5+
extends BaseProps,
6+
Omit<React.PropsWithoutRef<JSX.IntrinsicElements['div']>, 'title' | 'prefix'> {
7+
title?: React.ReactNode;
8+
value?: number | string;
9+
precision?: number;
10+
prefix?: React.ReactNode;
11+
suffix?: React.ReactNode;
12+
groupSeparator?: string;
13+
valueStyle?: React.CSSProperties;
14+
formatter?: (value: number | string) => React.ReactNode;
15+
}

0 commit comments

Comments
 (0)