Skip to content

Commit

Permalink
Merge pull request #641 from illacloud/fix/commnet_1130
Browse files Browse the repository at this point in the history
feat: add countup animation
  • Loading branch information
shandamengcheng committed Dec 13, 2022
2 parents 8689daf + 2e98fe6 commit ddab375
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 67 deletions.
3 changes: 3 additions & 0 deletions packages/statistic/src/interface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export interface StatisticProps
value?: string | number | Dayjs
decimalSeparator?: string
format?: string
countUp?: boolean
countDuration?: number
countFrom?: number
groupSeparator?: string
loading?: boolean
precision?: number
Expand Down
187 changes: 122 additions & 65 deletions packages/statistic/src/statistic.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { forwardRef, useMemo } from "react"
import {
forwardRef,
useMemo,
useEffect,
useRef,
useState,
useImperativeHandle,
MutableRefObject,
} from "react"
import { StatisticProps } from "./interface"
import { animate, AnimationPlaybackControls } from "framer-motion"
import { Skeleton } from "@illa-design/skeleton"
import dayjs, { Dayjs } from "dayjs"
import {
Expand All @@ -13,73 +22,121 @@ import {
import { isObject } from "@illa-design/system"
import { applyBoxStyle, deleteCssProps } from "@illa-design/theme"

export const Statistic = forwardRef<HTMLDivElement, StatisticProps>(
(props, ref) => {
const {
title,
value = 0,
decimalSeparator = ".",
format,
groupSeparator = ",",
loading,
precision,
suffix,
prefix,
extra,
...restProps
} = props
type StatisticHandle = {
onCountUp: () => void
}

const renderValue = useMemo<string | number | Dayjs>(() => {
if (format) {
return dayjs(value).format(format)
}
let temp: number | string = Number(value)
if (!isFinite(temp)) {
return value
export const Statistic = forwardRef<any, StatisticProps>((props, ref) => {
const {
title,
value = 0,
decimalSeparator = ".",
format,
groupSeparator = ",",
loading,
precision,
suffix,
prefix,
extra,
countUp,
countFrom = 0,
countDuration = 1,
...restProps
} = props

const [currentValue, setValue] = useState<string | number | Dayjs>(value)

const controlRef = useRef<AnimationPlaybackControls | null>()

const onCountUp = (from = countFrom, to = currentValue) => {
if (from !== to) {
controlRef.current = animate(from, Number(to), {
duration: countDuration,
ease: "easeOut",
onUpdate: (latest) => {
setValue(latest)
},
onComplete: () => {
setValue(to)
},
})
}
}

useEffect(() => {
if (props.countUp) {
if (controlRef.current) {
controlRef.current.stop()
}
if (precision !== void 0) {
temp = temp.toFixed(precision)
if (currentValue !== value) {
onCountUp(Number(currentValue), value)
} else {
onCountUp()
}
let [int, decimal] = String(temp).split(".")
int = int.replace(/\B(?=(\d{3})+(?!\d))/g, groupSeparator)
return decimal !== void 0 ? int + decimalSeparator + decimal : int
}, [format, value, groupSeparator, decimalSeparator, precision])
return (
<div
css={[statisticStyle, applyBoxStyle(props)]}
ref={ref}
{...deleteCssProps(restProps)}
>
{title && <div css={statisticTitleStyle}>{title}</div>}
<div>
<Skeleton
animation
visible={!!loading}
text={{ rows: 1, width: "100%" }}
>
<div css={statisticContentStyle}>
{prefix && (
<span
css={applyStatisticDecoratorStyle(true, !isObject(prefix))}
>
{prefix}
</span>
)}
<span css={statisticValueStyle}>{renderValue.toString()}</span>
{suffix && (
<span
css={applyStatisticDecoratorStyle(false, !isObject(suffix))}
>
{suffix}
</span>
)}
</div>
</Skeleton>
{extra && <div css={statisticExtraStyle}>{extra}</div>}
</div>
} else {
setValue(value)
}

return () => {
controlRef.current && controlRef.current.stop()
controlRef.current = null
}
}, [value])

useImperativeHandle<any, StatisticHandle>(ref, () => ({
onCountUp,
}))

const renderValue = useMemo<string | number | Dayjs>(() => {
if (format) {
return dayjs(currentValue).format(format)
}
let temp: number | string = Number(currentValue)
if (!isFinite(temp)) {
return currentValue
}
if (precision !== void 0) {
temp = temp.toFixed(precision)
}
let [int, decimal] = String(temp).split(".")
int = int.replace(/\B(?=(\d{3})+(?!\d))/g, groupSeparator)

return decimal !== void 0 ? int + decimalSeparator + decimal : int
}, [format, value, groupSeparator, decimalSeparator, precision, currentValue])

return (
<div
css={[statisticStyle, applyBoxStyle(props)]}
ref={ref}
{...deleteCssProps(restProps)}
>
{title && <div css={statisticTitleStyle}>{title}</div>}
<div>
<Skeleton
animation
visible={!!loading}
text={{ rows: 1, width: "100%" }}
>
<div css={statisticContentStyle}>
{prefix && (
<span css={applyStatisticDecoratorStyle(true, !isObject(prefix))}>
{prefix}
</span>
)}
<span css={statisticValueStyle}>{renderValue.toString()}</span>
{suffix && (
<span
css={applyStatisticDecoratorStyle(false, !isObject(suffix))}
>
{suffix}
</span>
)}
</div>
</Skeleton>
{extra && <div css={statisticExtraStyle}>{extra}</div>}
</div>
)
},
)
</div>
)
})

Statistic.displayName = "Statistic"
25 changes: 23 additions & 2 deletions packages/statistic/stories/statistic.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,37 @@
import { Meta, Story } from "@storybook/react"
import { Statistic, StatisticProps } from "../src"
import { Space, ImageDefaultIcon } from "@illa-design/react"
import { Space, ImageDefaultIcon, UpIcon, Button } from "@illa-design/react"
import React, { useRef } from "react"

export default {
title: "DATA DISPLAY/Statistic",
component: Statistic,
} as Meta

const Template: Story<StatisticProps> = (args) => {
let refGrowth = useRef<HTMLElement>(null)

return (
<Space size={"large"}>
<Statistic {...args} />
<Statistic {...args} title={<ImageDefaultIcon />} suffix={<UpIcon />} />
<Statistic
ref={(ref) => (refGrowth = ref)}
title="User Growth Rate"
value={123344}
precision={2}
prefix={<UpIcon />}
suffix="%"
countUp
/>
<Button
onClick={() => {
refGrowth && refGrowth.onCountUp()
}}
style={{ display: "block", marginTop: 10 }}
>
Start
</Button>
<Statistic
{...args}
title={<ImageDefaultIcon />}
Expand All @@ -23,7 +44,7 @@ const Template: Story<StatisticProps> = (args) => {
export const Basic = Template.bind({})
Basic.args = {
title: "Amount",
value: 0,
value: 12345,
prefix: "",
suffix: "",
}

0 comments on commit ddab375

Please sign in to comment.