首页/ mobileweb/ 图表工具大全 · 移动端数据可视化图表库(原生 JS 无依赖)/ 代码预览

💻 代码预览 · 图表工具大全 · 移动端数据可视化图表库(原生 JS 无依赖)

mw_tool · 上传者 驰旭教育
← 返回详情
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