Skip to content

Commit 8a66476

Browse files
authored
feat: donut chart on the popup page (#646) (#647)
1 parent 5112fbc commit 8a66476

File tree

11 files changed

+81
-29
lines changed

11 files changed

+81
-29
lines changed

src/i18n/message/popup/header-resource.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"en": {
33
"rate": "Rate Us",
44
"showSiteName": "Display site name",
5-
"showTopN": "Display top {n}"
5+
"showTopN": "Display top {n}",
6+
"donutChart": "Displayed as donut charts"
67
},
78
"zh_CN": {
89
"rate": "评分",

src/i18n/message/popup/header.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import resource from './header-resource.json'
99

1010
export type HeaderMessage = {
11+
donutChart: string
1112
rate: string
1213
showSiteName: string
1314
showTopN: string

src/pages/app/components/Analysis/components/Trend/Dimension/Chart.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import { type DimensionEntry, type ValueFormatter } from "@app/components/Analysis/util"
99
import { useEcharts } from "@hooks/useEcharts"
10-
import { defineComponent, watch } from "vue"
10+
import { defineComponent } from "vue"
1111
import Wrapper from "./Wrapper"
1212

1313
type Props = {
@@ -18,14 +18,15 @@ type Props = {
1818
}
1919

2020
const _default = defineComponent<Props>(props => {
21-
const { elRef, refresh } = useEcharts(Wrapper, () => ({
21+
const { elRef } = useEcharts(Wrapper, () => ({
2222
entries: props.data,
2323
preEntries: props.previous,
2424
title: props.title,
2525
valueFormatter: props.valueFormatter,
26-
}))
26+
}), {
27+
deps: [() => props.data, () => props.valueFormatter],
28+
})
2729

28-
watch([() => props.data, () => props.valueFormatter], refresh)
2930
return () => <div ref={elRef} style={{ width: '100%', height: '100%' }} />
3031
}, { props: ['data', 'previous', 'title', 'valueFormatter'] })
3132

src/pages/hooks/useEcharts.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { processAnimation, processAria, processFont, processRtl } from "@util/ec
1010
import { type AriaComponentOption, type ComposeOption, SeriesOption, TitleComponentOption } from "echarts"
1111
import { type ECharts, init } from "echarts/core"
1212
import { ElLoading } from "element-plus"
13-
import { type Ref, isRef, onMounted, ref, watch } from "vue"
13+
import { type Ref, type WatchSource, isRef, onMounted, ref, watch } from "vue"
1414
import { useElementSize } from './useElementSize'
1515
import { useWindowSize } from "./useWindowSize"
1616

@@ -86,7 +86,6 @@ export abstract class EchartsWrapper<BizOption, EchartsOption> {
8686

8787

8888
type WrapperResult<BizOption, EchartsOption, EW extends EchartsWrapper<BizOption, EchartsOption>> = {
89-
refresh: () => Promise<void>
9089
elRef: Ref<HTMLDivElement | undefined>
9190
wrapper: EW
9291
}
@@ -97,15 +96,17 @@ export const useEcharts = <BizOption, EchartsOption, EW extends EchartsWrapper<B
9796
option?: {
9897
hideLoading?: boolean
9998
manual?: boolean
100-
afterInit?: (ew: EW) => void
99+
afterInit?: ArgCallback<EW>,
100+
deps?: WatchSource | WatchSource[],
101101
}): WrapperResult<BizOption, EchartsOption, EW> => {
102102
const elRef = ref<HTMLDivElement>()
103103
const wrapperInstance = new Wrapper()
104104
const {
105105
hideLoading = false,
106106
manual = false,
107107
afterInit,
108-
} = option || {}
108+
deps,
109+
} = option ?? {}
109110

110111
let refresh = async () => {
111112
const loading = hideLoading ? null : ElLoading.service({ target: elRef.value })
@@ -124,13 +125,11 @@ export const useEcharts = <BizOption, EchartsOption, EW extends EchartsWrapper<B
124125
isRef(fetch) && watch(fetch, refresh)
125126
})
126127

128+
deps && watch(deps, refresh)
129+
127130
const { width: winW, height: winH } = useWindowSize()
128131
const { width: elW, height: elH } = useElementSize(elRef, { debounce: 50 })
129132
watch([winW, winH, elW, elH], () => wrapperInstance?.resize?.())
130133

131-
return {
132-
refresh,
133-
elRef,
134-
wrapper: wrapperInstance,
135-
}
134+
return { elRef, wrapper: wrapperInstance }
136135
}

src/pages/popup/components/Header/Option.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { ROUTE_PERCENTAGE } from '@popup/router'
77
import { ElCheckbox, ElIcon, ElInputNumber, ElPopover, ElText, useNamespace } from "element-plus"
88
import { computed, defineComponent, type StyleValue } from "vue"
99
import { useRoute } from 'vue-router'
10-
// import "./option.sass"
1110

1211
const reference = () => (
1312
<ElIcon size="large" style={{ cursor: 'pointer' } satisfies StyleValue}>
@@ -20,12 +19,16 @@ const reference = () => (
2019
const Option = defineComponent(() => {
2120
const option = useOption()
2221
const route = useRoute()
23-
const showTopN = computed(() => !!route.path?.endsWith(ROUTE_PERCENTAGE))
22+
const isPercentage = computed(() => !!route.path?.endsWith(ROUTE_PERCENTAGE))
2423

2524
const toggleName = () => {
2625
option.showName = !option.showName
2726
close()
2827
}
28+
const toggleDonutChart = () => {
29+
option.donutChart = !option.donutChart
30+
close()
31+
}
2932
const handleSiteChange = (v: number | undefined) => {
3033
if (!v) return
3134
option.topN = v
@@ -59,7 +62,11 @@ const Option = defineComponent(() => {
5962
<ElCheckbox size='small' modelValue={option.showName} />
6063
<ElText size='small'>{t(msg => msg.header.showSiteName)}</ElText>
6164
</Flex>
62-
<Flex v-show={showTopN.value}>
65+
<Flex v-show={isPercentage.value} gap={4} align='center' cursor="pointer" onClick={toggleDonutChart}>
66+
<ElCheckbox size='small' modelValue={!!option.donutChart} />
67+
<ElText size='small'>{t(msg => msg.header.donutChart)}</ElText>
68+
</Flex>
69+
<Flex v-show={isPercentage.value}>
6370
<ElText size='small'>
6471
{tN(msg => msg.header.showTopN, {
6572
n: <ElInputNumber

src/pages/popup/components/Percentage/Cate/Wrapper.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import {
1515
} from "echarts/components"
1616
import { type ComposeOption, type ECElementEvent } from "echarts/core"
1717
import {
18-
formatTooltip, generateSiteSeriesOption, generateTitleOption, generateToolboxOption, handleClick, isOther,
19-
type PieSeriesItemOption,
18+
adaptDonutSeries, formatTooltip, generateSiteSeriesOption, generateTitleOption, generateToolboxOption,
19+
handleClick, isOther, type PieSeriesItemOption,
2020
} from "../chart"
2121
import { type PercentageResult } from "../query"
2222

@@ -85,7 +85,7 @@ export default class SiteWrapper extends EchartsWrapper<PercentageResult, EcOpti
8585

8686
if (!result) return {}
8787

88-
const { rows, query } = result
88+
const { rows, query, donutChart } = result
8989
const { dimension } = query
9090
const selected: timer.stat.Row | undefined = this.selectedCache
9191
? rows.filter(isCate).filter(r => r.cateKey === this.selectedCache)[0]
@@ -113,8 +113,8 @@ export default class SiteWrapper extends EchartsWrapper<PercentageResult, EcOpti
113113
const series: PieSeriesOption[] = [{
114114
type: "pie",
115115
center: selected ? ['15%', '28%'] : ['58%', '56%'],
116-
radius: selected ? '30%' : '55%',
117116
selectedMode: 'single',
117+
selectedOffset: 5,
118118
startAngle: 180,
119119
data: rows.filter(isCate).map(row => ({
120120
value: row[dimension], row,
@@ -135,6 +135,8 @@ export default class SiteWrapper extends EchartsWrapper<PercentageResult, EcOpti
135135
position: selected ? 'inner' : 'outer',
136136
fontSize: selected ? 10 : undefined,
137137
},
138+
// not display as donut chart if selected
139+
...adaptDonutSeries(donutChart, selected ? '30%' : '55%', selected ? 0.3 : undefined),
138140
}]
139141
if (selected) {
140142
let mergedRows = (selected?.mergedRows || []).sort((a, b) => (b[dimension] ?? 0) - (a[dimension] ?? 0))

src/pages/popup/components/Percentage/Cate/index.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEcharts } from "@hooks/useEcharts"
22
import { usePopupContext } from "@popup/context"
3-
import { defineComponent, toRef, watch } from "vue"
3+
import { defineComponent, toRef } from "vue"
44
import { type PercentageResult } from "../query"
55
import Wrapper from "./Wrapper"
66

@@ -11,8 +11,7 @@ type Props = {
1111
const Cate = defineComponent<Props>(props => {
1212
const { darkMode } = usePopupContext()
1313
const data = toRef(props, 'value')
14-
const { elRef, refresh } = useEcharts(Wrapper, data)
15-
watch(darkMode, refresh)
14+
const { elRef } = useEcharts(Wrapper, data, { deps: darkMode })
1615

1716
return () => <div ref={elRef} style={{ width: '100%', height: '100%' }} />
1817
}, { props: ['value'] })

src/pages/popup/components/Percentage/Site/index.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEcharts } from "@hooks/useEcharts"
22
import { usePopupContext } from "@popup/context"
3-
import { defineComponent, toRef, watch } from "vue"
3+
import { defineComponent, toRef } from "vue"
44
import { type PercentageResult } from "../query"
55
import Wrapper from "./Wrapper"
66

@@ -11,8 +11,7 @@ type Props = {
1111
const Site = defineComponent<Props>(props => {
1212
const { darkMode } = usePopupContext()
1313
const data = toRef(props, 'value')
14-
const { elRef, refresh } = useEcharts(Wrapper, data)
15-
watch(darkMode, refresh)
14+
const { elRef } = useEcharts(Wrapper, data, { deps: darkMode })
1615

1716
return () => <div ref={elRef} style={{ width: '100%', height: '100%' }} />
1817
}, { props: ['value'] })

src/pages/popup/components/Percentage/chart.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ type CustomOption = Pick<
221221
>
222222

223223
export function generateSiteSeriesOption(rows: timer.stat.Row[], result: PercentageResult, customOption: CustomOption): PieSeriesOption {
224-
const { displaySiteName, query: { dimension }, itemCount, groups } = result || {}
224+
const { displaySiteName, query: { dimension }, itemCount, groups, donutChart } = result
225225
const groupMap = toMap(groups, g => g.id)
226226

227227
const chartRows = cvt2ChartRows(rows, dimension, itemCount)
@@ -268,7 +268,46 @@ export function generateSiteSeriesOption(rows: timer.stat.Row[], result: Percent
268268
},
269269
},
270270
avoidLabelOverlap: true,
271-
...customOption || {},
271+
...customOption,
272+
...adaptDonutSeries(donutChart, customOption.radius),
273+
}
274+
}
275+
276+
export function adaptDonutSeries(
277+
donutChart: boolean,
278+
radius: CustomOption['radius'],
279+
donutRadiusRatio: number = 0.5,
280+
): Pick<PieSeriesOption, 'itemStyle' | 'padAngle' | 'radius' | 'minAngle'> {
281+
return {
282+
...donutChart ? {
283+
itemStyle: { borderRadius: 5 },
284+
padAngle: 1,
285+
minAngle: 1.5,
286+
} : {
287+
itemStyle: { borderRadius: 0 },
288+
padAngle: 0,
289+
minAngle: 0,
290+
},
291+
radius: calcRealRadius(donutChart, radius, donutRadiusRatio),
292+
}
293+
}
294+
295+
function calcRealRadius(
296+
donutChart: boolean,
297+
radius: CustomOption['radius'],
298+
donutRadiusRatio: number = 0.5,
299+
): CustomOption['radius'] {
300+
if (!donutChart) return radius
301+
if (!radius) return radius
302+
if (Array.isArray(radius)) return radius
303+
if (typeof radius === 'number') return [radius * donutRadiusRatio, radius]
304+
// String
305+
try {
306+
const percent = parseFloat(radius.replace('%', ''))
307+
const inner = percent * donutRadiusRatio
308+
return [`${inner}%`, radius]
309+
} catch {
310+
return radius
272311
}
273312
}
274313

src/pages/popup/components/Percentage/query.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type PercentageResult = {
1515
itemCount: number
1616
dateLength: number
1717
groups: chrome.tabGroups.TabGroup[]
18+
donutChart: boolean
1819
}
1920

2021
const findAllDates = (row: timer.stat.Row): Set<string> => {
@@ -68,5 +69,6 @@ export const doQuery = async (query: PopupQuery, option: PopupOption): Promise<P
6869
chartTitle: t(msg => msg.content.percentage.title[query?.duration], { n: query?.durationNum }),
6970
itemCount,
7071
groups,
72+
donutChart: !!option.donutChart,
7173
} satisfies PercentageResult
7274
}

0 commit comments

Comments
 (0)