PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.5
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.5
4.9.5 4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 All 203 releases
betterdocs / react-src / admin / analytics / components / common / charts.js

charts.js in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.5, at react-src/admin/analytics/components/common/charts.js

166 lines 8.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React from "react";
2
3 /* Pure-SVG chart primitives ported from the design. */
4
5 export function Sparkline( { points, color = "#00b884", width = 100, height = 28, filled = true, strokeWidth = 1.5 } ) {
6 if ( ! points || points.length < 2 ) return null;
7 const max = Math.max( ...points ), min = Math.min( ...points ), range = max - min || 1;
8 const stepX = width / ( points.length - 1 );
9 const yOf = ( v ) => height - 2 - ( ( v - min ) / range ) * ( height - 4 );
10 const d = points.map( ( v, i ) => `${ i === 0 ? "M" : "L" }${ i * stepX },${ yOf( v ) }` ).join( " " );
11 const id = React.useId();
12 return (
13 <svg width={ width } height={ height } viewBox={ `0 0 ${ width } ${ height }` } preserveAspectRatio="none">
14 { filled && (
15 <>
16 <defs>
17 <linearGradient id={ id } x1="0" y1="0" x2="0" y2="1">
18 <stop offset="0%" stopColor={ color } stopOpacity="0.25" />
19 <stop offset="100%" stopColor={ color } stopOpacity="0" />
20 </linearGradient>
21 </defs>
22 <path d={ `${ d } L${ width },${ height } L0,${ height } Z` } fill={ `url(#${ id })` } />
23 </>
24 ) }
25 <path d={ d } fill="none" stroke={ color } strokeWidth={ strokeWidth } strokeLinecap="round" strokeLinejoin="round" />
26 </svg>
27 );
28 }
29
30 export function AreaChart( { series, labels, height = 240, colors, formatY = ( v ) => v } ) {
31 const ref = React.useRef( null );
32 const [ w, setW ] = React.useState( 800 );
33 const baseId = React.useId().replace( /:/g, "" );
34 React.useEffect( () => {
35 if ( ! ref.current ) return;
36 const ro = new ResizeObserver( ( entries ) => setW( entries[ 0 ].contentRect.width ) );
37 ro.observe( ref.current );
38 return () => ro.disconnect();
39 }, [] );
40
41 const padL = 44, padR = 16, padT = 12, padB = 28;
42 const innerW = Math.max( 100, w - padL - padR );
43 const innerH = height - padT - padB;
44 const allValues = series.flatMap( ( s ) => s.data );
45 const max = Math.max( ...allValues, 1 );
46 const pow = Math.pow( 10, Math.floor( Math.log10( max ) ) );
47 const niceMax = Math.ceil( max / pow ) * pow;
48 const yMax = Math.max( niceMax, max * 1.1 );
49 const xOf = ( i ) => padL + ( i / Math.max( 1, labels.length - 1 ) ) * innerW;
50 const yOf = ( v ) => padT + innerH - ( v / yMax ) * innerH;
51 const yTicks = 4;
52 const ticks = Array.from( { length: yTicks + 1 }, ( _, i ) => ( yMax / yTicks ) * i );
53 const xTickStep = Math.max( 1, Math.ceil( labels.length / 8 ) );
54 const [ hover, setHover ] = React.useState( null );
55
56 return (
57 <div ref={ ref } style={ { width: "100%", position: "relative" } }>
58 <svg width={ w } height={ height } style={ { display: "block" } }
59 onMouseLeave={ () => setHover( null ) }
60 onMouseMove={ ( e ) => {
61 const rect = e.currentTarget.getBoundingClientRect();
62 const i = Math.round( ( ( e.clientX - rect.left - padL ) / innerW ) * ( labels.length - 1 ) );
63 setHover( i >= 0 && i < labels.length ? i : null );
64 } }>
65 { ticks.map( ( t, i ) => (
66 <g key={ i }>
67 <line x1={ padL } x2={ padL + innerW } y1={ yOf( t ) } y2={ yOf( t ) } stroke="#EDF0F3" strokeWidth="1" />
68 <text x={ padL - 8 } y={ yOf( t ) + 4 } textAnchor="end" fontSize="11" fill="#94A3B8">{ formatY( Math.round( t ) ) }</text>
69 </g>
70 ) ) }
71 { labels.map( ( l, i ) => ( i % xTickStep === 0 || i === labels.length - 1 ) ? (
72 <text key={ i } x={ xOf( i ) } y={ height - 8 } textAnchor="middle" fontSize="11" fill="#94A3B8">{ l }</text>
73 ) : null ) }
74 { series.map( ( s, si ) => {
75 const color = s.color || colors?.[ si ] || "#00b884";
76 const d = s.data.map( ( v, i ) => `${ i === 0 ? "M" : "L" }${ xOf( i ) },${ yOf( v ) }` ).join( " " );
77 const area = `${ d } L${ xOf( s.data.length - 1 ) },${ padT + innerH } L${ xOf( 0 ) },${ padT + innerH } Z`;
78 const gid = `g${ baseId }${ si }`;
79 return (
80 <g key={ si }>
81 <defs>
82 <linearGradient id={ gid } x1="0" y1="0" x2="0" y2="1">
83 <stop offset="0%" stopColor={ color } stopOpacity="0.20" />
84 <stop offset="100%" stopColor={ color } stopOpacity="0" />
85 </linearGradient>
86 </defs>
87 <path d={ area } fill={ `url(#${ gid })` } />
88 <path d={ d } fill="none" stroke={ color } strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
89 </g>
90 );
91 } ) }
92 { hover !== null && (
93 <g>
94 <line x1={ xOf( hover ) } x2={ xOf( hover ) } y1={ padT } y2={ padT + innerH } stroke="#CBD5E1" strokeWidth="1" strokeDasharray="3 3" />
95 { series.map( ( s, si ) => (
96 <circle key={ si } cx={ xOf( hover ) } cy={ yOf( s.data[ hover ] ) } r="4" fill="#fff" stroke={ s.color || colors?.[ si ] || "#00b884" } strokeWidth="2" />
97 ) ) }
98 </g>
99 ) }
100 </svg>
101 { hover !== null && (
102 <div style={ {
103 position: "absolute", pointerEvents: "none", background: "#0f172a", color: "#fff",
104 fontSize: "12px", borderRadius: "8px", padding: "8px 12px", boxShadow: "0 8px 24px rgba(0,0,0,.2)",
105 left: `${ Math.max( 8, Math.min( w - 200, xOf( hover ) - 90 ) ) }px`, top: `${ padT }px`, minWidth: "160px"
106 } }>
107 <div style={ { color: "#cbd5e1", marginBottom: "6px" } }>{ labels[ hover ] }</div>
108 { series.map( ( s, si ) => (
109 <div key={ si } style={ { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "12px" } }>
110 <span style={ { display: "flex", alignItems: "center", gap: "6px" } }>
111 <span style={ { width: 8, height: 8, borderRadius: 9999, background: s.color || colors?.[ si ] || "#00b884" } } />
112 <span style={ { color: "#e2e8f0" } }>{ s.name }</span>
113 </span>
114 <span style={ { fontWeight: 600, fontVariantNumeric: "tabular-nums" } }>{ formatY( s.data[ hover ] ) }</span>
115 </div>
116 ) ) }
117 </div>
118 ) }
119 </div>
120 );
121 }
122
123 export function Donut( { segments, size = 180, thickness = 26, centerLabel, centerValue } ) {
124 const cx = size / 2, cy = size / 2, r = ( size - thickness ) / 2;
125 const total = segments.reduce( ( s, x ) => s + x.value, 0 ) || 1;
126 let acc = 0;
127 const arcs = segments.map( ( seg ) => {
128 const start = ( acc / total ) * Math.PI * 2 - Math.PI / 2;
129 acc += seg.value;
130 const end = ( acc / total ) * Math.PI * 2 - Math.PI / 2;
131 const large = end - start > Math.PI ? 1 : 0;
132 return {
133 d: `M${ cx + Math.cos( start ) * r },${ cy + Math.sin( start ) * r } A${ r },${ r } 0 ${ large } 1 ${ cx + Math.cos( end ) * r },${ cy + Math.sin( end ) * r }`,
134 color: seg.color
135 };
136 } );
137 return (
138 <svg width={ size } height={ size } style={ { flexShrink: 0 } }>
139 <circle cx={ cx } cy={ cy } r={ r } fill="none" stroke="#F1F5F9" strokeWidth={ thickness } />
140 { arcs.map( ( a, i ) => <path key={ i } d={ a.d } fill="none" stroke={ a.color } strokeWidth={ thickness } /> ) }
141 { centerValue != null && (
142 <>
143 <text x={ cx } y={ cy - 4 } textAnchor="middle" fontSize="22" fontWeight="700" fill="#0F172A">{ centerValue }</text>
144 <text x={ cx } y={ cy + 16 } textAnchor="middle" fontSize="11" fill="#64748B">{ centerLabel }</text>
145 </>
146 ) }
147 </svg>
148 );
149 }
150
151 export function HBar( { items, max, color = "#00b884", format = ( v ) => v } ) {
152 const m = max || Math.max( ...items.map( ( i ) => i.value ), 1 );
153 return (
154 <div className="bda-hbar">
155 { items.map( ( it, i ) => (
156 <div key={ i } className="bda-hbar-row">
157 <div className="bda-hbar-name">{ it.name }</div>
158 <div className="bda-hbar-track"><div className="bda-hbar-fill" style={ { width: `${ ( it.value / m ) * 100 }%`, background: it.color || color } } /></div>
159 <div className="bda-hbar-val">{ format( it.value ) }</div>
160 </div>
161 ) ) }
162 </div>
163 );
164 }
165
166