PluginProbe
Code Snippets / 4.0.0-beta.2
Code Snippets v4.0.0-beta.2
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 3.0.0 All 65 releases
code-snippets / js / components / InsightsMenu / InsightsCharts.tsx

InsightsCharts.tsx in Code Snippets 4.0.0-beta.2, at js/components/InsightsMenu/InsightsCharts.tsx

164 lines 4.9 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 { InsightsChartViewToggle } from './InsightsChartViewToggle'
5 import type { InsightsChartEntry, InsightsChartKey, InsightsChartViews, InsightsConfigurableChartKey } from '../../types/Insights'
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, InsightsChartEntry>>,
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, InsightsChartEntry>>
36 colors?: Readonly<Record<string, string>>
37 }
38
39 const EntryLabel: React.FC<Pick<InsightsChartEntry, 'label' | 'url'>> = ({ label, url }) =>
40 url ? <a className="insights-chart-entry-link" href={url}>{label}</a> : <>{label}</>
41
42 const BarChart: React.FC<ChartProps> = ({ colors, entries }) => {
43 const entryCounts = useMemo(() =>
44 Object.values(entries)
45 .map(entry => Number(entry.count)),
46 [entries])
47
48 return (
49 <ul className="insights-bar-chart">
50 {Object.entries(entries).map(([key, entry]) =>
51 <li key={key}>
52 <span><EntryLabel {...entry} /></span>
53 <div className="insights-bar-track" aria-hidden="true">
54 <div
55 className="insights-bar-fill"
56 style={{
57 backgroundColor: colors?.[key] ?? DEFAULT_COLOR,
58 inlineSize: `${Number(entry.count) / Math.max(1, ...entryCounts) * PERCENTAGE_MAX}%`
59 }}
60 />
61 </div>
62 <strong>{entry.count}</strong>
63 </li>)}
64 </ul>
65 )
66 }
67
68 const PieChart: React.FC<ChartProps> = ({ colors, entries }) => {
69 const totalCount = useMemo(() =>
70 Object.values(entries).reduce((count, entry) =>
71 count + Number(entry.count), 0),
72 [entries])
73
74 return (
75 <div className="insights-pie-chart-content">
76 <div
77 className={classnames('insights-pie-chart', { 'is-empty': 0 === totalCount })}
78 aria-hidden="true"
79 style={0 === totalCount ? undefined : { background: getPieBackground(entries, colors, totalCount) }}
80 />
81 <ul className="insights-pie-chart-legend">
82 {Object.entries(entries).map(([key, entry]) =>
83 <li key={key}>
84 <span>
85 <i aria-hidden="true" style={{ backgroundColor: colors?.[key] ?? DEFAULT_COLOR }} />
86 <EntryLabel {...entry} />
87 </span>
88 <strong>{entry.count}</strong>
89 </li>)}
90 </ul>
91 </div>
92 )
93 }
94
95 const TagCloud: React.FC<ChartProps> = ({ entries }) => {
96 const largestCount = useMemo(() =>
97 Math.max(1, ...Object.values(entries).map(entry => Number(entry.count))),
98 [entries])
99
100 return (
101 <ul className="insights-tags-cloud">
102 {Object.entries(entries).map(([key, entry]) =>
103 <li
104 key={key}
105 style={{ fontSize: `${TAG_CLOUD_MIN_FONT_SIZE + Number(entry.count) / largestCount * TAG_CLOUD_FONT_SIZE_RANGE}rem` }}
106 >
107 <EntryLabel {...entry} />
108 <span className="screen-reader-text">{sprintf(
109 _n(' (%s snippet)', ' (%s snippets)', Number(entry.count), 'code-snippets'),
110 entry.count
111 )}</span>
112 </li>)}
113 </ul>
114 )
115 }
116
117 export interface InsightsChartProps<Chart extends InsightsConfigurableChartKey> {
118 chart: Chart
119 entries: Readonly<Record<string, InsightsChartEntry>>
120 title: string
121 view: InsightsChartViews[Chart]
122 setView?: (view: InsightsChartViews[Chart]) => void
123 colors?: Readonly<Record<string, string>>
124 views: readonly InsightsChartViews[Chart][]
125 }
126
127 export const InsightsChart = <Chart extends InsightsConfigurableChartKey,>({
128 chart,
129 colors,
130 entries,
131 setView,
132 title,
133 view,
134 views
135 }: InsightsChartProps<Chart>) =>
136 <section
137 className="insights-chart-card"
138 data-insights-chart={chart}
139 data-view={view}
140 aria-labelledby={`insights-chart-${chart}-heading`}
141 >
142 <div className="insights-chart-card-header">
143 <h2 id={`insights-chart-${chart}-heading`}>{title}</h2>
144 {setView && <InsightsChartViewToggle title={title} view={view} setView={setView} views={views} />}
145 </div>
146 {'bar' === view
147 ? <BarChart colors={colors} entries={entries} />
148 : 'pie' === view
149 ? <PieChart colors={colors} entries={entries} />
150 : <TagCloud entries={entries} />}
151 </section>
152
153 export interface TotalsInsightsChartProps extends InsightsChartEntry {
154 chart: InsightsChartKey
155 }
156
157 export const TotalsInsightsChart: React.FC<TotalsInsightsChartProps> = ({ chart, count, label }) =>
158 <section className="insights-chart-card" data-insights-chart={chart} aria-label={label}>
159 <div className="insights-number-chart">
160 <strong className="insights-number-chart-value">{count}</strong>
161 <span className="insights-number-chart-label">{label}</span>
162 </div>
163 </section>
164