r111/chart.js · 约 35014 字符
1/* r111 图表工具大全 · 纯 SVG 图表引擎(零依赖,不使用任何第三方图表库) */2var CH = (function () {3 'use strict';4 var NS = 'http://www.w3.org/2000/svg';5 var PAL = ['#4263eb', '#12b886', '#f59f00', '#e8590c', '#7048e8', '#e64980', '#1098ad', '#82c91e', '#fa5252', '#5c7cfa'];67 function E(n, a, t) {8 var e = document.createElementNS(NS, n);9 if (a) for (var k in a) { if (a[k] !== null && a[k] !== undefined) e.setAttribute(k, a[k]); }10 if (t !== undefined) e.textContent = t;11 return e;12 }13 function mk(w, h) {14 return E('svg', { viewBox: '0 0 ' + w + ' ' + h, width: '100%', preserveAspectRatio: 'xMidYMid meet' });15 }16 function clear(host) {17 var t = host.querySelector('.chtip');18 while (host.firstChild) host.removeChild(host.firstChild);19 if (t) host.appendChild(t);20 }21 function num(v) {22 var a = Math.abs(v);23 if (a >= 100000000) return (v / 100000000).toFixed(1) + '亿';24 if (a >= 10000) return (v / 10000).toFixed(a % 10000 === 0 ? 0 : 1) + 'w';25 if (a >= 1000) return (v / 1000).toFixed(1) + 'k';26 return String(Math.round(v * 100) / 100);27 }28 function thousand(v) {29 var s = (Math.round(v * 100) / 100).toString().split('.');30 s[0] = s[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');31 return s.join('.');32 }33 function ticks(min, max, cnt) {34 cnt = cnt || 4;35 if (min === max) { max = min + (min === 0 ? 1 : Math.abs(min) * 0.2); }36 var span = max - min;37 var step = Math.pow(10, Math.floor(Math.log(span / cnt) / Math.LN10));38 var err = span / cnt / step;39 if (err >= 7.5) step *= 10; else if (err >= 3) step *= 5; else if (err >= 1.5) step *= 2;40 var s = Math.floor(min / step) * step, e = Math.ceil(max / step) * step;41 var out = [], v = s, guard = 0;42 while (v <= e + step * 0.5 && guard++ < 40) { out.push(Math.round(v * 1e6) / 1e6); v += step; }43 return out;44 }45 function tipbox(host) {46 var t = host.querySelector('.chtip');47 if (!t) { t = document.createElement('div'); t.className = 'chtip'; host.appendChild(t); }48 return t;49 }50 function hideTip(host) { var t = host.querySelector('.chtip'); if (t) t.style.display = 'none'; }51 function showTip(host, x, y, html) {52 var t = tipbox(host);53 t.innerHTML = html;54 t.style.display = 'block';55 var w = host.clientWidth || 340;56 t.style.left = Math.max(56, Math.min(w - 56, x)) + 'px';57 t.style.top = Math.max(28, y) + 'px';58 }59 function tipRow(color, name, val) {60 return '<div class="tk"><s class="dot" style="background:' + color + '"></s>' + name + '<b>' + val + '</b></div>';61 }62 function autoHide(host) {63 clearTimeout(host._tt);64 host._tt = setTimeout(function () { hideTip(host); }, 2800);65 }66 function animPath(p) {67 try {68 var len = p.getTotalLength();69 p.style.strokeDasharray = len;70 p.style.strokeDashoffset = len;71 requestAnimationFrame(function () {72 p.style.transition = 'stroke-dashoffset .85s cubic-bezier(.25,.8,.3,1)';73 p.style.strokeDashoffset = 0;74 });75 } catch (e) { }76 }77 function fadeIn(node, delay) {78 node.style.opacity = 0;79 setTimeout(function () { node.style.transition = 'opacity .4s'; node.style.opacity = 1; }, delay || 0);80 }8182 /* ---------------- 折线路径构造:直线 / 平滑曲线 / 阶梯 ---------------- */83 function buildPath(pts, mode) {84 if (!pts.length) return '';85 var i, d;86 if (mode === 'step') {87 d = 'M' + pts[0][0] + ',' + pts[0][1];88 for (i = 1; i < pts.length; i++) {89 var mx = (pts[i - 1][0] + pts[i][0]) / 2;90 d += 'L' + mx + ',' + pts[i - 1][1] + 'L' + mx + ',' + pts[i][1] + 'L' + pts[i][0] + ',' + pts[i][1];91 }92 return d;93 }94 if (mode !== 'smooth') {95 d = 'M' + pts[0][0] + ',' + pts[0][1];96 for (i = 1; i < pts.length; i++) d += 'L' + pts[i][0] + ',' + pts[i][1];97 return d;98 }99 d = 'M' + pts[0][0] + ',' + pts[0][1];100 for (i = 0; i < pts.length - 1; i++) {101 var p0 = pts[i - 1] || pts[i], p1 = pts[i], p2 = pts[i + 1], p3 = pts[i + 2] || p2;102 var c1x = p1[0] + (p2[0] - p0[0]) / 6, c1y = p1[1] + (p2[1] - p0[1]) / 6;103 var c2x = p2[0] - (p3[0] - p1[0]) / 6, c2y = p2[1] - (p3[1] - p1[1]) / 6;104 d += 'C' + c1x.toFixed(2) + ',' + c1y.toFixed(2) + ' ' + c2x.toFixed(2) + ',' + c2y.toFixed(2) + ' ' + p2[0].toFixed(2) + ',' + p2[1].toFixed(2);105 }106 return d;107 }108109 /* ---------------- 直角坐标系网格与刻度 ---------------- */110 function frame(s, o, y0, y1, tk, PL, PT, iw, ih, W, H, cats, horiz) {111 var i, y, x;112 if (horiz) {113 for (i = 0; i < tk.length; i++) {114 x = PL + (tk[i] - y0) / (y1 - y0) * iw;115 s.appendChild(E('line', { x1: x, y1: PT, x2: x, y2: PT + ih, stroke: '#eef1f7', 'stroke-width': 1 }));116 s.appendChild(E('text', { x: x, y: H - 8, 'text-anchor': 'middle', 'font-size': 9.5, fill: '#98a2b3' }, num(tk[i])));117 }118 for (i = 0; i < cats.length; i++) {119 y = PT + ih * (i + 0.5) / cats.length;120 s.appendChild(E('text', { x: PL - 6, y: y + 3.5, 'text-anchor': 'end', 'font-size': 10, fill: '#5b6577' }, cats[i]));121 }122 return;123 }124 for (i = 0; i < tk.length; i++) {125 y = PT + ih - (tk[i] - y0) / (y1 - y0) * ih;126 s.appendChild(E('line', { x1: PL, y1: y, x2: PL + iw, y2: y, stroke: tk[i] === 0 ? '#d8dee9' : '#eef1f7', 'stroke-width': 1 }));127 s.appendChild(E('text', { x: PL - 6, y: y + 3.5, 'text-anchor': 'end', 'font-size': 9.5, fill: '#98a2b3' }, num(tk[i])));128 }129 var step = Math.ceil(cats.length / 7);130 for (i = 0; i < cats.length; i++) {131 if (i % step !== 0 && i !== cats.length - 1) continue;132 x = cats.length > 1 ? PL + iw * i / (cats.length - 1) : PL + iw / 2;133 if (o.band) x = PL + iw * (i + 0.5) / cats.length;134 s.appendChild(E('text', { x: x, y: H - 8, 'text-anchor': 'middle', 'font-size': 9.5, fill: '#98a2b3' }, cats[i]));135 }136 }137138 /* ================================================================ 折线 / 曲线 / 阶梯 / 面积 / 堆叠 */139 function line(host, o) {140 clear(host);141 var W = 360, H = o.h || 230, PL = 40, PR = 14, PT = 16, PB = 26;142 var cats = o.cats || [];143 var all = o.series || [];144 var ss = all.filter(function (x) { return !x.off; });145 var stack = !!o.stack, pct = !!o.percent;146 var i, j, vals = [], stacked = [];147 if (stack) {148 for (i = 0; i < cats.length; i++) {149 var acc = 0, col = [];150 for (j = 0; j < ss.length; j++) { acc += (ss[j].data[i] || 0); col.push(acc); }151 stacked.push(col);152 }153 if (pct) {154 for (i = 0; i < cats.length; i++) {155 var tot = stacked[i][ss.length - 1] || 1;156 for (j = 0; j < ss.length; j++) stacked[i][j] = stacked[i][j] / tot * 100;157 }158 }159 for (i = 0; i < stacked.length; i++) vals.push(stacked[i][ss.length - 1] || 0);160 vals.push(0);161 } else {162 for (j = 0; j < ss.length; j++) for (i = 0; i < ss[j].data.length; i++) vals.push(ss[j].data[i]);163 }164 if (!vals.length) vals = [0, 1];165 var mn = Math.min.apply(null, vals), mx = Math.max.apply(null, vals);166 if (o.zero !== false) mn = Math.min(0, mn);167 var tk = pct ? [0, 25, 50, 75, 100] : ticks(mn, mx, 4);168 var y0 = tk[0], y1 = tk[tk.length - 1];169 var iw = W - PL - PR, ih = H - PT - PB;170 var X = function (k) { return cats.length > 1 ? PL + iw * k / (cats.length - 1) : PL + iw / 2; };171 var Y = function (v) { return PT + ih - (v - y0) / (y1 - y0) * ih; };172 var s = mk(W, H);173 frame(s, o, y0, y1, tk, PL, PT, iw, ih, W, H, cats, false);174 var defs = E('defs');175 s.appendChild(defs);176177 var order = stack ? ss.slice().reverse() : ss;178 for (j = 0; j < order.length; j++) {179 var sr = order[j];180 var realIdx = ss.indexOf(sr);181 var color = sr.color || PAL[realIdx % PAL.length];182 var pts = [];183 for (i = 0; i < cats.length; i++) {184 var v = stack ? stacked[i][realIdx] : (sr.data[i] || 0);185 pts.push([X(i), Y(v)]);186 }187 if (o.area || stack) {188 var gid = 'g' + Math.random().toString(36).slice(2, 9);189 var lg = E('linearGradient', { id: gid, x1: 0, y1: 0, x2: 0, y2: 1 });190 lg.appendChild(E('stop', { offset: '0%', 'stop-color': color, 'stop-opacity': stack ? 0.88 : 0.34 }));191 lg.appendChild(E('stop', { offset: '100%', 'stop-color': color, 'stop-opacity': stack ? 0.6 : 0.02 }));192 defs.appendChild(lg);193 var base = Y(Math.max(y0, 0));194 var ad = buildPath(pts, o.mode) + 'L' + pts[pts.length - 1][0] + ',' + base + 'L' + pts[0][0] + ',' + base + 'Z';195 var ap = E('path', { d: ad, fill: 'url(#' + gid + ')', stroke: 'none' });196 s.appendChild(ap);197 fadeIn(ap, 200);198 }199 var lp = E('path', {200 d: buildPath(pts, o.mode), fill: 'none', stroke: color,201 'stroke-width': o.lw || 2.2, 'stroke-linejoin': 'round', 'stroke-linecap': 'round'202 });203 s.appendChild(lp);204 animPath(lp);205 if (o.dots !== false && cats.length <= 14) {206 for (i = 0; i < pts.length; i++) {207 var c = E('circle', { cx: pts[i][0], cy: pts[i][1], r: 3, fill: '#fff', stroke: color, 'stroke-width': 1.8 });208 s.appendChild(c);209 fadeIn(c, 480 + i * 22);210 }211 }212 sr._color = color;213 }214 for (j = 0; j < ss.length; j++) { if (!ss[j]._color) ss[j]._color = PAL[j % PAL.length]; }215216 var mark = E('line', { x1: 0, y1: PT, x2: 0, y2: PT + ih, stroke: '#4263eb', 'stroke-width': 1, 'stroke-dasharray': '3 3' });217 mark.style.display = 'none';218 s.appendChild(mark);219 var ov = E('rect', { x: PL, y: PT, width: iw, height: ih, fill: 'transparent' });220 s.appendChild(ov);221 host.appendChild(s);222223 function pick(ev) {224 var r = s.getBoundingClientRect();225 var cx = (ev.touches ? ev.touches[0].clientX : ev.clientX) - r.left;226 var vx = cx / r.width * W;227 var idx = cats.length > 1 ? Math.round((vx - PL) / (iw / (cats.length - 1))) : 0;228 idx = Math.max(0, Math.min(cats.length - 1, idx));229 mark.setAttribute('x1', X(idx)); mark.setAttribute('x2', X(idx)); mark.style.display = '';230 var total = 0, k;231 for (k = 0; k < ss.length; k++) total += (ss[k].data[idx] || 0);232 var html = '<div class="tt">' + cats[idx] + '</div>';233 for (k = 0; k < ss.length; k++) {234 var raw = ss[k].data[idx] || 0;235 var txt = pct ? ((raw / (total || 1) * 100).toFixed(1) + '%') : (thousand(raw) + (o.unit || ''));236 html += tipRow(ss[k]._color, ss[k].name, txt);237 }238 showTip(host, X(idx) / W * (host.clientWidth || W), 30, html);239 }240 ov.addEventListener('touchstart', pick, { passive: true });241 ov.addEventListener('touchmove', pick, { passive: true });242 ov.addEventListener('mousemove', pick);243 var hide = function () { mark.style.display = 'none'; hideTip(host); };244 ov.addEventListener('touchend', hide);245 ov.addEventListener('mouseleave', hide);246 return s;247 }248249 /* ================================================================ 柱状 / 条形 / 分组 / 堆叠 */250 function bar(host, o) {251 clear(host);252 var horiz = !!o.horizontal, stack = !!o.stack;253 var W = 360, H = o.h || 230;254 var PL = horiz ? 52 : 40, PR = 14, PT = 16, PB = 26;255 var cats = o.cats || [];256 var ss = (o.series || []).filter(function (x) { return !x.off; });257 var i, j, vals = [];258 if (stack) {259 for (i = 0; i < cats.length; i++) { var t = 0; for (j = 0; j < ss.length; j++) t += (ss[j].data[i] || 0); vals.push(t); }260 } else {261 for (j = 0; j < ss.length; j++) for (i = 0; i < ss[j].data.length; i++) vals.push(ss[j].data[i]);262 }263 vals.push(0);264 var tk = ticks(Math.min.apply(null, vals), Math.max.apply(null, vals), 4);265 var y0 = tk[0], y1 = tk[tk.length - 1];266 var iw = W - PL - PR, ih = H - PT - PB;267 var s = mk(W, H);268 o.band = true;269 frame(s, o, y0, y1, tk, PL, PT, iw, ih, W, H, cats, horiz);270271 var slot = (horiz ? ih : iw) / Math.max(1, cats.length);272 var gw = slot * 0.62;273 var bw = stack ? gw : gw / Math.max(1, ss.length);274 var zeroX = PL + (0 - y0) / (y1 - y0) * iw;275 for (i = 0; i < cats.length; i++) {276 var acc = 0;277 for (j = 0; j < ss.length; j++) {278 var sr = ss[j], v = sr.data[i] || 0;279 var color = sr.color || PAL[j % PAL.length];280 if (horiz) {281 var yy = PT + slot * i + (slot - gw) / 2 + (stack ? 0 : bw * j);282 var xs = stack ? PL + (acc - y0) / (y1 - y0) * iw : zeroX;283 var wlen = Math.abs(v) / (y1 - y0) * iw;284 var rn = E('rect', { x: xs, y: yy, width: 0, height: Math.max(2, bw - 2.5), rx: 3, fill: color });285 s.appendChild(rn);286 (function (n, tw, dl) {287 setTimeout(function () { n.style.transition = 'width .68s cubic-bezier(.25,.8,.3,1)'; n.setAttribute('width', Math.max(1, tw)); }, dl);288 })(rn, wlen, 40 + (i * ss.length + j) * 26);289 } else {290 var xx = PL + slot * i + (slot - gw) / 2 + (stack ? 0 : bw * j);291 var top = stack ? acc + v : Math.max(v, 0);292 var ty = PT + ih - (top - y0) / (y1 - y0) * ih;293 var hgt = Math.abs(v) / (y1 - y0) * ih;294 var rn2 = E('rect', { x: xx, y: PT + ih, width: Math.max(2, bw - 2.5), height: 0, rx: 3, fill: color });295 s.appendChild(rn2);296 (function (n, yv, hv, dl) {297 setTimeout(function () {298 n.style.transition = 'height .68s cubic-bezier(.25,.8,.3,1), y .68s cubic-bezier(.25,.8,.3,1)';299 n.setAttribute('y', yv); n.setAttribute('height', Math.max(1, hv));300 }, dl);301 })(rn2, ty, hgt, 40 + (i * ss.length + j) * 26);302 }303 if (o.label && !stack && ss.length === 1) {304 var lx = horiz ? zeroX + Math.abs(v) / (y1 - y0) * iw + 5 : PL + slot * i + slot / 2;305 var ly = horiz ? PT + slot * i + slot / 2 + 3.5 : PT + ih - Math.abs(v) / (y1 - y0) * ih - 5;306 var tn = E('text', { x: lx, y: ly, 'text-anchor': horiz ? 'start' : 'middle', 'font-size': 9.5, fill: '#5b6577', 'font-weight': 700 }, num(v));307 s.appendChild(tn);308 fadeIn(tn, 640);309 }310 acc += v;311 }312 (function (idx) {313 var hit = horiz314 ? E('rect', { x: PL, y: PT + slot * idx, width: iw, height: slot, fill: 'transparent' })315 : E('rect', { x: PL + slot * idx, y: PT, width: slot, height: ih, fill: 'transparent' });316 hit.style.cursor = 'pointer';317 hit.addEventListener('click', function () {318 var html = '<div class="tt">' + cats[idx] + '</div>';319 for (var k = 0; k < ss.length; k++) {320 html += tipRow(ss[k].color || PAL[k % PAL.length], ss[k].name, thousand(ss[k].data[idx] || 0) + (o.unit || ''));321 }322 var cx = horiz ? (host.clientWidth || W) / 2 : (PL + slot * (idx + 0.5)) / W * (host.clientWidth || W);323 showTip(host, cx, horiz ? (PT + slot * idx) * 0.7 + 34 : 34, html);324 autoHide(host);325 });326 s.appendChild(hit);327 })(i);328 }329 host.appendChild(s);330 return s;331 }332333 /* ================================================================ 扇形路径 */334 function arcPath(cx, cy, r0, r1, a0, a1) {335 var p = Math.PI / 180;336 var x0 = cx + r1 * Math.cos(a0 * p), y0 = cy + r1 * Math.sin(a0 * p);337 var x1 = cx + r1 * Math.cos(a1 * p), y1 = cy + r1 * Math.sin(a1 * p);338 var x2 = cx + r0 * Math.cos(a1 * p), y2 = cy + r0 * Math.sin(a1 * p);339 var x3 = cx + r0 * Math.cos(a0 * p), y3 = cy + r0 * Math.sin(a0 * p);340 var big = (a1 - a0) > 180 ? 1 : 0;341 var d = 'M' + x0.toFixed(2) + ',' + y0.toFixed(2) + 'A' + r1.toFixed(2) + ',' + r1.toFixed(2) + ' 0 ' + big + ' 1 ' + x1.toFixed(2) + ',' + y1.toFixed(2);342 if (r0 > 0) {343 d += 'L' + x2.toFixed(2) + ',' + y2.toFixed(2) + 'A' + r0.toFixed(2) + ',' + r0.toFixed(2) + ' 0 ' + big + ' 0 ' + x3.toFixed(2) + ',' + y3.toFixed(2) + 'Z';344 } else {345 d += 'L' + cx + ',' + cy + 'Z';346 }347 return d;348 }349350 /* ================================================================ 饼图 / 环形图 / 玫瑰图 */351 function pie(host, o) {352 clear(host);353 var W = 360, H = o.h || 250;354 var items = (o.items || []).filter(function (x) { return !x.off; });355 var cx = W / 2, cy = H / 2 - 2;356 var R = Math.min(W, H) / 2 - 24;357 var inner = o.inner ? R * o.inner : 0;358 var total = 0, i, maxV = 0;359 for (i = 0; i < items.length; i++) { total += items[i].value; maxV = Math.max(maxV, items[i].value); }360 if (!total) total = 1;361 if (!maxV) maxV = 1;362 var s = mk(W, H);363 var ang = -90;364 for (i = 0; i < items.length; i++) {365 var it = items[i];366 var sweep = o.rose ? 360 / items.length : it.value / total * 360;367 var rr = o.rose ? (R * 0.38 + R * 0.62 * (it.value / maxV)) : R;368 var color = it.color || PAL[i % PAL.length];369 var gap = items.length > 1 ? 0.7 : 0;370 var p = E('path', { d: arcPath(cx, cy, inner, rr, ang + gap, ang + sweep - gap), fill: color, stroke: '#fff', 'stroke-width': 1 });371 p.style.opacity = 0;372 p.style.transformOrigin = cx + 'px ' + cy + 'px';373 p.style.transform = 'scale(.7)';374 p.style.cursor = 'pointer';375 s.appendChild(p);376 (function (n, dl) {377 setTimeout(function () {378 n.style.transition = 'opacity .4s, transform .55s cubic-bezier(.25,1.2,.4,1)';379 n.style.opacity = 1; n.style.transform = 'scale(1)';380 }, dl);381 })(p, 60 + i * 70);382 if (o.showLabel !== false && (o.rose || it.value / total > 0.055)) {383 var mid = (ang + sweep / 2) * Math.PI / 180;384 var lr = inner > 0 ? (inner + rr) / 2 : rr * 0.64;385 var lb = E('text', {386 x: cx + lr * Math.cos(mid), y: cy + lr * Math.sin(mid) + 3.5,387 'text-anchor': 'middle', 'font-size': 10, fill: '#fff', 'font-weight': 700, 'pointer-events': 'none'388 }, (it.value / total * 100).toFixed(0) + '%');389 s.appendChild(lb);390 fadeIn(lb, 520);391 }392 (function (n, item, c) {393 n.addEventListener('click', function () {394 showTip(host, (host.clientWidth || W) / 2, 28,395 '<div class="tt">' + (o.title || '构成占比') + '</div>' +396 tipRow(c, item.name, thousand(item.value) + (o.unit || '') + ' · ' + (item.value / total * 100).toFixed(1) + '%'));397 autoHide(host);398 });399 })(p, it, color);400 ang += sweep;401 }402 if (inner > 0) {403 s.appendChild(E('text', { x: cx, y: cy - 5, 'text-anchor': 'middle', 'font-size': 11, fill: '#98a2b3' }, o.centerLabel || '合计'));404 s.appendChild(E('text', { x: cx, y: cy + 16, 'text-anchor': 'middle', 'font-size': 18, fill: '#1f2430', 'font-weight': 800 },405 o.centerValue || (num(total) + (o.unit || ''))));406 }407 host.appendChild(s);408 return s;409 }410411 /* ================================================================ 散点图 / 气泡图 */412 function scatter(host, o) {413 clear(host);414 var W = 360, H = o.h || 250, PL = 40, PR = 16, PT = 18, PB = 34;415 var ss = (o.series || []).filter(function (x) { return !x.off; });416 var xs = [], ys = [], zs = [], i, j;417 for (j = 0; j < ss.length; j++) for (i = 0; i < ss[j].points.length; i++) {418 xs.push(ss[j].points[i][0]); ys.push(ss[j].points[i][1]); zs.push(ss[j].points[i][2] || 1);419 }420 if (!xs.length) { xs = [0, 1]; ys = [0, 1]; zs = [1]; }421 var xt = ticks(Math.min(0, Math.min.apply(null, xs)), Math.max.apply(null, xs), 4);422 var yt = ticks(Math.min(0, Math.min.apply(null, ys)), Math.max.apply(null, ys), 4);423 var x0 = xt[0], x1 = xt[xt.length - 1], y0 = yt[0], y1 = yt[yt.length - 1];424 var zmax = Math.max.apply(null, zs) || 1;425 var iw = W - PL - PR, ih = H - PT - PB;426 var X = function (v) { return PL + (v - x0) / (x1 - x0) * iw; };427 var Y = function (v) { return PT + ih - (v - y0) / (y1 - y0) * ih; };428 var s = mk(W, H);429 for (i = 0; i < yt.length; i++) {430 var yy = Y(yt[i]);431 s.appendChild(E('line', { x1: PL, y1: yy, x2: PL + iw, y2: yy, stroke: '#eef1f7', 'stroke-width': 1 }));432 s.appendChild(E('text', { x: PL - 6, y: yy + 3.5, 'text-anchor': 'end', 'font-size': 9.5, fill: '#98a2b3' }, num(yt[i])));433 }434 for (i = 0; i < xt.length; i++) {435 var xx = X(xt[i]);436 s.appendChild(E('line', { x1: xx, y1: PT, x2: xx, y2: PT + ih, stroke: '#f4f6fb', 'stroke-width': 1 }));437 s.appendChild(E('text', { x: xx, y: PT + ih + 14, 'text-anchor': 'middle', 'font-size': 9.5, fill: '#98a2b3' }, num(xt[i])));438 }439 if (o.xname) s.appendChild(E('text', { x: PL + iw / 2, y: H - 4, 'text-anchor': 'middle', 'font-size': 9.5, fill: '#98a2b3' }, o.xname));440 if (o.yname) s.appendChild(E('text', { x: PL - 2, y: 10, 'text-anchor': 'start', 'font-size': 9.5, fill: '#98a2b3' }, o.yname));441 for (j = 0; j < ss.length; j++) {442 var sr = ss[j], color = sr.color || PAL[j % PAL.length];443 for (i = 0; i < sr.points.length; i++) {444 var pt = sr.points[i];445 var r = o.bubble ? 4 + 13 * Math.sqrt((pt[2] || 1) / zmax) : 4.5;446 var c = E('circle', { cx: X(pt[0]), cy: Y(pt[1]), r: 0, fill: color, 'fill-opacity': o.bubble ? 0.5 : 0.9, stroke: color, 'stroke-width': 1.3 });447 c.style.cursor = 'pointer';448 s.appendChild(c);449 (function (n, rr, dl) { setTimeout(function () { n.style.transition = 'r .5s cubic-bezier(.25,1.3,.4,1)'; n.setAttribute('r', rr); }, dl); })(c, r, 50 + (j * 8 + i) * 24);450 (function (n, p, nm, col) {451 n.addEventListener('click', function () {452 showTip(host, X(p[0]) / W * (host.clientWidth || W), Math.max(30, Y(p[1]) - 14),453 '<div class="tt">' + nm + '</div>' +454 tipRow(col, o.xname || 'X', thousand(p[0])) +455 tipRow(col, o.yname || 'Y', thousand(p[1])) +456 (p[2] !== undefined ? tipRow(col, o.zname || 'Z', thousand(p[2])) : ''));457 autoHide(host);458 });459 })(c, pt, sr.name, color);460 }461 }462 host.appendChild(s);463 return s;464 }465466 /* ================================================================ 雷达图 */467 function radar(host, o) {468 clear(host);469 var W = 360, H = o.h || 280;470 var ind = o.indicators || [];471 var ss = (o.series || []).filter(function (x) { return !x.off; });472 var cx = W / 2, cy = H / 2, R = Math.min(W, H) / 2 - 42;473 var n = ind.length, i, j;474 if (!n) return null;475 var s = mk(W, H);476 var A = function (k) { return (-90 + 360 * k / n) * Math.PI / 180; };477 for (var lv = 4; lv >= 1; lv--) {478 var rr = R * lv / 4, d = '';479 for (i = 0; i < n; i++) d += (i ? 'L' : 'M') + (cx + rr * Math.cos(A(i))).toFixed(2) + ',' + (cy + rr * Math.sin(A(i))).toFixed(2);480 s.appendChild(E('path', { d: d + 'Z', fill: lv % 2 ? '#fafbfe' : '#ffffff', stroke: '#e9edf5', 'stroke-width': 1 }));481 }482 for (i = 0; i < n; i++) {483 s.appendChild(E('line', { x1: cx, y1: cy, x2: cx + R * Math.cos(A(i)), y2: cy + R * Math.sin(A(i)), stroke: '#e9edf5', 'stroke-width': 1 }));484 var co = Math.cos(A(i));485 var anc = Math.abs(co) < 0.25 ? 'middle' : (co > 0 ? 'start' : 'end');486 s.appendChild(E('text', {487 x: cx + (R + 17) * co, y: cy + (R + 17) * Math.sin(A(i)) + 3.5,488 'text-anchor': anc, 'font-size': 10.5, fill: '#5b6577', 'font-weight': 600489 }, ind[i].name));490 }491 for (j = 0; j < ss.length; j++) {492 var sr = ss[j], color = sr.color || PAL[j % PAL.length], d2 = '';493 for (i = 0; i < n; i++) {494 var ratio = (sr.data[i] || 0) / (ind[i].max || 100);495 d2 += (i ? 'L' : 'M') + (cx + R * ratio * Math.cos(A(i))).toFixed(2) + ',' + (cy + R * ratio * Math.sin(A(i))).toFixed(2);496 }497 var pg = E('path', { d: d2 + 'Z', fill: color, 'fill-opacity': 0.18, stroke: color, 'stroke-width': 2, 'stroke-linejoin': 'round' });498 pg.style.transformOrigin = cx + 'px ' + cy + 'px';499 pg.style.transform = 'scale(.25)';500 pg.style.opacity = 0;501 s.appendChild(pg);502 (function (nd, dl) {503 setTimeout(function () {504 nd.style.transition = 'transform .7s cubic-bezier(.25,1.1,.35,1), opacity .5s';505 nd.style.transform = 'scale(1)'; nd.style.opacity = 1;506 }, dl);507 })(pg, 80 + j * 150);508 for (i = 0; i < n; i++) {509 var rt = (sr.data[i] || 0) / (ind[i].max || 100);510 var dot = E('circle', { cx: cx + R * rt * Math.cos(A(i)), cy: cy + R * rt * Math.sin(A(i)), r: 3, fill: '#fff', stroke: color, 'stroke-width': 1.8 });511 s.appendChild(dot);512 fadeIn(dot, 680 + i * 28);513 }514 }515 host.appendChild(s);516 return s;517 }518519 /* ================================================================ 仪表盘 */520 function gauge(host, o) {521 clear(host);522 var W = 360, H = o.h || 190;523 var cx = W / 2, cy = H - 32, R = Math.min(W / 2 - 40, H - 54);524 var s = mk(W, H);525 var color = o.color || '#4263eb';526 var mxv = o.max || 100;527 s.appendChild(E('path', { d: arcPath(cx, cy, R - 15, R, 180, 360), fill: '#eef1f7' }));528 var arc = E('path', { d: arcPath(cx, cy, R - 15, R, 180, 180.01), fill: color });529 s.appendChild(arc);530 for (var i = 0; i <= 5; i++) {531 var ag = (180 + 36 * i) * Math.PI / 180;532 s.appendChild(E('text', {533 x: cx + (R - 27) * Math.cos(ag), y: cy + (R - 27) * Math.sin(ag) + 3,534 'text-anchor': 'middle', 'font-size': 9, fill: '#98a2b3'535 }, String(Math.round(mxv * i / 5))));536 }537 var vt = E('text', { x: cx, y: cy - 10, 'text-anchor': 'middle', 'font-size': 24, fill: '#1f2430', 'font-weight': 800 }, '0');538 s.appendChild(vt);539 s.appendChild(E('text', { x: cx, y: cy + 13, 'text-anchor': 'middle', 'font-size': 11, fill: '#98a2b3' }, o.name || ''));540 host.appendChild(s);541 var pct = Math.max(0, Math.min(1, (o.value || 0) / mxv));542 var t0 = Date.now();543 (function step() {544 var k = Math.min(1, (Date.now() - t0) / 900);545 var e = 1 - Math.pow(1 - k, 3);546 arc.setAttribute('d', arcPath(cx, cy, R - 15, R, 180, 180 + Math.max(0.01, 180 * pct * e)));547 vt.textContent = ((o.value || 0) * e).toFixed(1) + (o.unit || '');548 if (k < 1) requestAnimationFrame(step);549 })();550 return s;551 }552553 /* ================================================================ 漏斗图 */554 function funnel(host, o) {555 clear(host);556 var items = (o.items || []).filter(function (x) { return !x.off; });557 var n = items.length;558 if (!n) return null;559 var W = 360, H = o.h || (n * 46 + 16);560 var s = mk(W, H);561 var maxV = 0, i;562 for (i = 0; i < n; i++) maxV = Math.max(maxV, items[i].value);563 if (!maxV) maxV = 1;564 var gapY = 6, hh = (H - 16 - gapY * (n - 1)) / n;565 var cx = W / 2 - 24, span = W - 130;566 for (i = 0; i < n; i++) {567 var it = items[i];568 var w0 = it.value / maxV * span;569 var next = i < n - 1 ? items[i + 1].value : it.value * 0.85;570 var w1 = next / maxV * span;571 var y = 8 + i * (hh + gapY);572 var color = it.color || PAL[i % PAL.length];573 var p = E('path', {574 d: 'M' + (cx - w0 / 2) + ',' + y + 'L' + (cx + w0 / 2) + ',' + y +575 'L' + (cx + w1 / 2) + ',' + (y + hh) + 'L' + (cx - w1 / 2) + ',' + (y + hh) + 'Z',576 fill: color, 'fill-opacity': 0.93577 });578 p.style.opacity = 0;579 p.style.transformOrigin = cx + 'px ' + (y + hh / 2) + 'px';580 p.style.transform = 'scaleX(.25)';581 s.appendChild(p);582 (function (nd, dl) {583 setTimeout(function () {584 nd.style.transition = 'opacity .35s, transform .6s cubic-bezier(.25,1,.4,1)';585 nd.style.opacity = 1; nd.style.transform = 'scaleX(1)';586 }, dl);587 })(p, 60 + i * 105);588 var tn = E('text', { x: cx, y: y + hh / 2 + 4, 'text-anchor': 'middle', 'font-size': 11, fill: '#fff', 'font-weight': 700 },589 it.name + ' ' + num(it.value));590 s.appendChild(tn);591 fadeIn(tn, 240 + i * 105);592 var rate = i === 0 ? 100 : it.value / items[0].value * 100;593 var rn = E('text', { x: W - 6, y: y + hh / 2 + 4, 'text-anchor': 'end', 'font-size': 10, fill: '#5b6577', 'font-weight': 700 },594 rate.toFixed(1) + '%');595 s.appendChild(rn);596 fadeIn(rn, 300 + i * 105);597 }598 host.appendChild(s);599 return s;600 }601602 /* ================================================================ 日历热力图 */603 function heat(host, o) {604 clear(host);605 var vals = o.values || [];606 var weeks = o.weeks || Math.ceil(vals.length / 7);607 var days = o.days || ['一', '二', '三', '四', '五', '六', '日'];608 var W = 360, gap = 3, PL = 22, PT = 14;609 var cw = Math.max(8, (W - PL - 12) / weeks - gap);610 var ch = Math.min(24, cw + 2);611 var H = PT + 7 * (ch + gap) + 8;612 var s = mk(W, H);613 var mx = Math.max.apply(null, vals.length ? vals : [1]) || 1;614 var base = o.rgb || [66, 99, 235];615 var i, di;616 for (di = 0; di < 7; di++) {617 s.appendChild(E('text', { x: PL - 6, y: PT + di * (ch + gap) + ch / 2 + 3.5, 'text-anchor': 'end', 'font-size': 9, fill: '#98a2b3' }, days[di]));618 }619 for (i = 0; i < vals.length; i++) {620 var wi = Math.floor(i / 7);621 di = i % 7;622 var alpha = 0.08 + 0.92 * Math.pow(vals[i] / mx, 0.75);623 var r = E('rect', {624 x: PL + 4 + wi * (cw + gap), y: PT + di * (ch + gap),625 width: cw, height: ch, rx: 3.5,626 fill: 'rgba(' + base[0] + ',' + base[1] + ',' + base[2] + ',' + alpha.toFixed(3) + ')'627 });628 r.style.cursor = 'pointer';629 s.appendChild(r);630 fadeIn(r, 20 + i * 5);631 (function (nd, idx, x) {632 nd.addEventListener('click', function () {633 var d = new Date(o.start || '2026-01-01');634 d.setDate(d.getDate() + idx);635 var ds = d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2);636 showTip(host, x / W * (host.clientWidth || W), 24,637 '<div class="tt">' + ds + '</div>' + tipRow('rgb(' + base.join(',') + ')', o.name || '活跃', thousand(vals[idx]) + (o.unit || '')));638 autoHide(host);639 });640 })(r, i, PL + 4 + wi * (cw + gap) + cw / 2);641 }642 host.appendChild(s);643 return s;644 }645646 /* ================================================================ 迷你缩略图(图表库卡片) */647 function mini(host, kind, color) {648 clear(host);649 color = color || '#4263eb';650 var W = 120, H = 46, s = mk(W, H), i;651 var seed = [8, 22, 14, 30, 20, 38, 28, 42];652 if (kind === 'line' || kind === 'curve' || kind === 'area') {653 var pts = [];654 for (i = 0; i < seed.length; i++) pts.push([6 + i * (W - 12) / (seed.length - 1), H - 4 - seed[i]]);655 if (kind === 'area') {656 s.appendChild(E('path', {657 d: buildPath(pts, 'smooth') + 'L' + pts[pts.length - 1][0] + ',' + (H - 2) + 'L' + pts[0][0] + ',' + (H - 2) + 'Z',658 fill: color, 'fill-opacity': 0.2659 }));660 }661 s.appendChild(E('path', { d: buildPath(pts, kind === 'line' ? 'line' : 'smooth'), fill: 'none', stroke: color, 'stroke-width': 2, 'stroke-linecap': 'round' }));662 } else if (kind === 'bar' || kind === 'stack') {663 for (i = 0; i < 6; i++) {664 var bh = 10 + (i * 7 % 24);665 s.appendChild(E('rect', { x: 9 + i * 18, y: H - 3 - bh, width: 11, height: bh, rx: 2, fill: color, 'fill-opacity': 0.88 }));666 if (kind === 'stack') s.appendChild(E('rect', { x: 9 + i * 18, y: H - 4 - bh - 8, width: 11, height: 8, rx: 2, fill: '#12b886', 'fill-opacity': 0.85 }));667 }668 } else if (kind === 'pie' || kind === 'donut' || kind === 'rose') {669 var cx = W / 2, cy = H / 2, R = 20, inn = kind === 'donut' ? 11 : 0;670 var segs = [40, 25, 20, 15], a = -90;671 var cols = [color, '#12b886', '#f59f00', '#e64980'];672 for (i = 0; i < segs.length; i++) {673 var sw = segs[i] / 100 * 360;674 var rr = kind === 'rose' ? R * (0.55 + 0.45 * segs[i] / 40) : R;675 s.appendChild(E('path', { d: arcPath(cx, cy, inn, rr, a + 1, a + sw - 1), fill: cols[i] }));676 a += sw;677 }678 } else if (kind === 'scatter') {679 var xs2 = [12, 28, 40, 55, 70, 84, 98, 108], ys2 = [34, 26, 30, 18, 22, 12, 16, 8];680 for (i = 0; i < xs2.length; i++) s.appendChild(E('circle', { cx: xs2[i], cy: ys2[i], r: i % 3 === 0 ? 5.5 : 3.2, fill: color, 'fill-opacity': 0.55 }));681 } else if (kind === 'radar') {682 var rx = W / 2, ry = H / 2, R2 = 19, n2 = 5, d3 = '', d4 = '';683 var rts = [0.92, 0.6, 0.82, 0.5, 0.74];684 for (i = 0; i < n2; i++) {685 var ag = (-90 + 360 * i / n2) * Math.PI / 180;686 d3 += (i ? 'L' : 'M') + (rx + R2 * Math.cos(ag)).toFixed(1) + ',' + (ry + R2 * Math.sin(ag)).toFixed(1);687 d4 += (i ? 'L' : 'M') + (rx + R2 * rts[i] * Math.cos(ag)).toFixed(1) + ',' + (ry + R2 * rts[i] * Math.sin(ag)).toFixed(1);688 }689 s.appendChild(E('path', { d: d3 + 'Z', fill: 'none', stroke: '#e9edf5' }));690 s.appendChild(E('path', { d: d4 + 'Z', fill: color, 'fill-opacity': 0.24, stroke: color, 'stroke-width': 1.6 }));691 } else if (kind === 'funnel') {692 var ws = [92, 72, 54, 34], yy = 5;693 var fc = [color, '#5c7cfa', '#12b886', '#f59f00'];694 for (i = 0; i < ws.length; i++) {695 var w2 = ws[i], w3 = ws[i + 1] || 20;696 s.appendChild(E('path', {697 d: 'M' + (W / 2 - w2 / 2) + ',' + yy + 'L' + (W / 2 + w2 / 2) + ',' + yy +698 'L' + (W / 2 + w3 / 2) + ',' + (yy + 8) + 'L' + (W / 2 - w3 / 2) + ',' + (yy + 8) + 'Z',699 fill: fc[i]700 }));701 yy += 10;702 }703 } else if (kind === 'gauge') {704 var gx = W / 2, gy = H - 8, GR = 22;705 s.appendChild(E('path', { d: arcPath(gx, gy, GR - 7, GR, 180, 360), fill: '#eef1f7' }));706 s.appendChild(E('path', { d: arcPath(gx, gy, GR - 7, GR, 180, 302), fill: color }));707 } else if (kind === 'heat') {708 for (i = 0; i < 28; i++) {709 s.appendChild(E('rect', {710 x: 10 + (i % 7) * 15, y: 6 + Math.floor(i / 7) * 9.5, width: 12, height: 7.5, rx: 2,711 fill: color, 'fill-opacity': 0.12 + 0.8 * ((i * 37 % 11) / 10)712 }));713 }714 }715 host.appendChild(s);716 return s;717 }718719 /* ================================================================ 图例 */720 function legend(host, series, onToggle) {721 host.innerHTML = '';722 series.forEach(function (sr, i) {723 var b = document.createElement('i');724 if (sr.off) b.className = 'off';725 b.innerHTML = '<s style="background:' + (sr.color || PAL[i % PAL.length]) + '"></s>' + sr.name;726 b.addEventListener('click', function () {727 var alive = series.filter(function (x) { return !x.off; }).length;728 if (!sr.off && alive <= 1) return;729 sr.off = !sr.off;730 b.className = sr.off ? 'off' : '';731 if (onToggle) onToggle();732 });733 host.appendChild(b);734 });735 }736737 return {738 line: line, bar: bar, pie: pie, scatter: scatter, radar: radar,739 gauge: gauge, funnel: funnel, heat: heat, mini: mini, legend: legend,740 palette: PAL, num: num, thousand: thousand, ticks: ticks, arcPath: arcPath, svg: mk, el: E741 };742})();743