PluginProbe
Code Snippets / 4.0.0-beta.3
Code Snippets v4.0.0-beta.3
4.0.0-beta.3 4.0.0-beta.2 3.10.2 3.10.1 3.10.0 3.10.0-beta.2 3.10.0-beta.1 4.0.0-beta.1 3.9.6 trunk 2.10.0 2.10.1 2.12.0 2.12.1 2.13.0 2.13.1 2.13.2 2.13.3 2.14.0 2.14.1 2.14.2 2.14.3 2.14.4 2.14.5 2.14.6 All 66 releases
code-snippets / js / components / StatsMenu / StatsCharts.tsx

StatsCharts.tsx in Code Snippets 4.0.0-beta.3, at js/components/StatsMenu/StatsCharts.tsx

172 lines 5.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { _n, sprintf } from '@wordpress/i18n'
2 import classnames from 'classnames'
3 import React, { useMemo } from 'react'
4 import { StatsChartViewToggle } from './StatsChartViewToggle'
5 import type { StatsChartEntry, StatsChartKey, StatsChartViews, StatsConfigurableChartKey } from '../../types/Stats'
6
7 const PERCENTAGE_MAX = 100
8
9 const DEFAULT_COLOR = '#646970'
10
11 const TAG_CLOUD_MIN_FONT_SIZE = 0.875
12
13 const TAG_CLOUD_FONT_SIZE_RANGE = 0.625
14
15 const getPieBackground = (
16 entries: Readonly<Record<string, StatsChartEntry>>,
17 colors: Readonly<Record<string, string>> | undefined,
18 totalCount: number
19 ): string => {
20 let start = 0
21 const segments = Object.entries(entries)
22 .filter(([, entry]) => 0 < Number(entry.count))
23 .map(([key, entry]) => {
24 const end = start + Number(entry.count) / totalCount * PERCENTAGE_MAX
25 const segment = `${colors?.[key] ?? DEFAULT_COLOR} ${start}% ${end}%`
26
27 start = end
28 return segment
29 })
30
31 return `conic-gradient(${segments.join(', ')})`
32 }
33
34 interface ChartProps {
35 entries: Readonly<Record<string, StatsChartEntry>>
36 colors?: Readonly<Record<string, string>>
37 }
38
39 const EntryLabel: React.FC<Pick<StatsChartEntry, 'label' | 'url'>> = ({ label, url }) =>
40 url ? <a className="stats-chart-entry-link" href={url}>{label}</a> : <>{label}</>
41
42 const BarChart: React.FC<ChartProps & { overlay?: React.ReactNode }> = ({ colors, entries, overlay }) => {
43 const entryCounts = useMemo(() =>
44 Object.values(entries)
45 .map(entry => Number(entry.count)),
46 [entries])
47
48 return (
49 <div className="stats-bar-chart-content-container">
50 <ul className="stats-bar-chart">
51 {Object.entries(entries).map(([key, entry]) =>
52 <li key={key}>
53 <span><EntryLabel {...entry} /></span>
54 <div className="stats-bar-track" aria-hidden="true">
55 <div
56 className="stats-bar-fill"
57 style={{
58 backgroundColor: colors?.[key] ?? DEFAULT_COLOR,
59 inlineSize: `${Number(entry.count) / Math.max(1, ...entryCounts) * PERCENTAGE_MAX}%`
60 }}
61 />
62 </div>
63 <strong>{entry.count}</strong>
64 </li>)}
65 </ul>
66 {overlay && <div className="stats-lock">{overlay}</div>}
67 </div>
68 )
69 }
70
71 const PieChart: React.FC<ChartProps & { overlay?: React.ReactNode }> = ({ colors, entries, overlay }) => {
72 const totalCount = useMemo(() =>
73 Object.values(entries).reduce((count, entry) =>
74 count + Number(entry.count), 0),
75 [entries])
76
77 return (
78 <div className="stats-pie-chart-content-container">
79 <div className="stats-pie-chart-content">
80 <div
81 className={classnames('stats-pie-chart', { 'is-empty': 0 === totalCount })}
82 aria-hidden="true"
83 style={0 === totalCount ? undefined : { background: getPieBackground(entries, colors, totalCount) }}
84 />
85 <ul className="stats-pie-chart-legend">
86 {Object.entries(entries).map(([key, entry]) =>
87 <li key={key}>
88 <span>
89 <i aria-hidden="true" style={{ backgroundColor: colors?.[key] ?? DEFAULT_COLOR }} />
90 <EntryLabel {...entry} />
91 </span>
92 <strong>{entry.count}</strong>
93 </li>)}
94 </ul>
95 </div>
96 {overlay && <div className="stats-lock">{overlay}</div>}
97 </div>
98 )
99 }
100
101 const TagCloud: React.FC<ChartProps> = ({ entries }) => {
102 const largestCount = useMemo(() =>
103 Math.max(1, ...Object.values(entries).map(entry => Number(entry.count))),
104 [entries])
105
106 return (
107 <ul className="stats-tags-cloud">
108 {Object.entries(entries).map(([key, entry]) =>
109 <li
110 key={key}
111 style={{ fontSize: `${TAG_CLOUD_MIN_FONT_SIZE + Number(entry.count) / largestCount * TAG_CLOUD_FONT_SIZE_RANGE}rem` }}
112 >
113 <EntryLabel {...entry} />
114 <span className="screen-reader-text">{sprintf(
115 _n(' (%s snippet)', ' (%s snippets)', Number(entry.count), 'code-snippets'),
116 entry.count
117 )}</span>
118 </li>)}
119 </ul>
120 )
121 }
122
123 export interface StatsChartProps<Chart extends StatsConfigurableChartKey> {
124 chart: Chart
125 entries: Readonly<Record<string, StatsChartEntry>>
126 overlay?: React.ReactNode
127 title: string
128 view: StatsChartViews[Chart]
129 setView?: (view: StatsChartViews[Chart]) => void
130 colors?: Readonly<Record<string, string>>
131 views: readonly StatsChartViews[Chart][]
132 }
133
134 export const StatsChart = <Chart extends StatsConfigurableChartKey,>({
135 chart,
136 colors,
137 entries,
138 overlay,
139 setView,
140 title,
141 view,
142 views
143 }: StatsChartProps<Chart>) =>
144 <section
145 className="stats-chart-card"
146 data-stats-chart={chart}
147 data-view={view}
148 aria-labelledby={`stats-chart-${chart}-heading`}
149 >
150 <div className="stats-chart-card-header">
151 <h2 id={`stats-chart-${chart}-heading`}>{title}</h2>
152 {setView && <StatsChartViewToggle title={title} view={view} setView={setView} views={views} />}
153 </div>
154 {'bar' === view
155 ? <BarChart colors={colors} entries={entries} overlay={overlay} />
156 : 'pie' === view
157 ? <PieChart colors={colors} entries={entries} overlay={overlay} />
158 : <TagCloud entries={entries} />}
159 </section>
160
161 export interface TotalsStatsChartProps extends StatsChartEntry {
162 chart: StatsChartKey
163 }
164
165 export const TotalsStatsChart: React.FC<TotalsStatsChartProps> = ({ chart, count, label }) =>
166 <section className="stats-chart-card" data-stats-chart={chart} aria-label={label}>
167 <div className="stats-number-chart">
168 <strong className="stats-number-chart-value">{count}</strong>
169 <span className="stats-number-chart-label">{label}</span>
170 </div>
171 </section>
172