{
  "Name": "Диаграмма лучей",
  "Code": "(function () {\n  var container = document.getElementById(w.general.renderTo);\n  var props = w.props || {};\n  var items = w.data?.primaryData?.items || [];\n\n  if (!items.length) {\n    container.innerHTML =\n      '<div style=\"display:flex;align-items:center;justify-content:center;height:100%;color:#999;\">Нет данных</div>';\n    return;\n  }\n\n  // ===== Свойства =====\n  var maxDepth = props.maxDepth != null ? Number(props.maxDepth) : 3;\n  if (maxDepth < 1) maxDepth = 1;\n  var animationEnabled = props.animationEnabled !== false;\n  var animationDuration = props.animationDuration != null ? Number(props.animationDuration) : 1000;\n  var otherThresholdPct = props.otherThresholdPct != null ? Number(props.otherThresholdPct) : 3;\n\n  var showLabels = props.showLabels !== false;\n  var labelAutoColor = props.labelAutoColor !== false;\n  var labelColor = props.labelColor || '#ffffff';\n  var labelStrokeColor = props.labelStrokeColor || '#000000';\n  var labelStrokeWidth = props.labelStrokeWidth != null ? Number(props.labelStrokeWidth) : 2;\n  var labelRotate = (props.labelRotate || 'radial').toLowerCase();\n  var labelAlign = (props.labelAlign || 'center').toLowerCase();\n  var labelFontFamily = props.labelFontFamily || 'sans-serif';\n  var labelFontSize = props.labelFontSize != null ? Number(props.labelFontSize) : 12;\n  var labelFontWeight = props.labelFontWeight || 'normal';\n\n  var tooltipEnabled = props.tooltipEnabled !== false;\n  var tooltipTemplate = props.tooltipTemplate || '{path}<br/>Значение: <b>{value}</b> ({percent}%)';\n\n  var gradientEnabled = props.gradientEnabled === true;\n  var gradientColorLow = props.gradientColorLow || '#cfe8ff';\n  var gradientColorHigh = props.gradientColorHigh || '#08306b';\n  var conditionRulesJson = props.conditionRulesJson || '';\n\n  var showLegend = props.showLegend !== false;\n  var legendPosition = (props.legendPosition || 'top-left').toLowerCase();\n\n  // ===== Цветовые утилиты =====\n  function hexToRgb(hex) {\n    hex = String(hex).replace('#', '');\n    if (hex.length === 3) hex = hex.split('').map(function (c) { return c + c; }).join('');\n    var n = parseInt(hex, 16);\n    return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };\n  }\n  function rgbToHex(r, g, b) {\n    return '#' + [r, g, b].map(function (x) {\n      var s = Math.round(Math.max(0, Math.min(255, x))).toString(16);\n      return s.length === 1 ? '0' + s : s;\n    }).join('');\n  }\n  function lighten(hex, amt) {\n    var c = hexToRgb(hex);\n    return rgbToHex(c.r + (255 - c.r) * amt, c.g + (255 - c.g) * amt, c.b + (255 - c.b) * amt);\n  }\n  function lerpColor(a, b, t) {\n    var ca = hexToRgb(a), cb = hexToRgb(b);\n    return rgbToHex(ca.r + (cb.r - ca.r) * t, ca.g + (cb.g - ca.g) * t, ca.b + (cb.b - ca.b) * t);\n  }\n  function fmt(v) {\n    try { return Number(v).toLocaleString('ru-RU'); } catch (e) { return String(v); }\n  }\n\n  // ===== Условное форматирование (условия) =====\n  var rules = [];\n  if (conditionRulesJson) {\n    try { rules = JSON.parse(conditionRulesJson); } catch (e) { console.warn('conditionRulesJson invalid', e); }\n  }\n  function evalCond(v, op, t) {\n    switch (op) {\n      case '>': return v > t;\n      case '>=': return v >= t;\n      case '<': return v < t;\n      case '<=': return v <= t;\n      case '==': case '=': return v == t;\n      case '!=': return v != t;\n      case 'between': return Array.isArray(t) && v >= t[0] && v <= t[1];\n    }\n    return false;\n  }\n  function applyRules(value) {\n    for (var i = 0; i < rules.length; i++) {\n      var r = rules[i];\n      if (evalCond(value, r.op, r.value)) return r.color;\n    }\n    return null;\n  }\n\n  // ===== Построение иерархического дерева =====\n  var root = { name: 'root', value: 0, _map: {}, children: [] };\n  function addPath(keys, value) {\n    var node = root;\n    node.value += value;\n    for (var i = 0; i < keys.length; i++) {\n      var k = keys[i];\n      if (!node._map[k]) {\n        var child = { name: k, value: 0, _map: {}, children: [] };\n        node._map[k] = child;\n        node.children.push(child);\n      }\n      node = node._map[k];\n      node.value += value;\n    }\n  }\n  items.forEach(function (it) {\n    var keys = (it.keys || []).map(function (k) { return k == null ? '' : String(k); })\n      .filter(function (k) { return k !== ''; });\n    var v = it.values && it.values.length ? Number(it.values[0]) || 0 : 0;\n    if (keys.length) addPath(keys, v);\n  });\n\n  // ===== Глубина + группировка «Прочее» =====\n  function pruneChildren(node, curDepth) {\n    if (!node.children || !node.children.length) return;\n    if (curDepth >= maxDepth) { node.children = []; return; }\n    if (otherThresholdPct > 0 && node.value > 0) {\n      var keep = [], small = [];\n      node.children.forEach(function (c) {\n        if ((c.value / node.value * 100) < otherThresholdPct) small.push(c); else keep.push(c);\n      });\n      if (small.length > 1) {\n        var sumv = small.reduce(function (s, c) { return s + c.value; }, 0);\n        keep.push({ name: 'Прочее', value: sumv, children: [], _other: true });\n        node.children = keep;\n      }\n    }\n    node.children.forEach(function (c) { pruneChildren(c, curDepth + 1); });\n  }\n  pruneChildren(root, 0);\n\n  // ===== Диапазон значений листьев (для градиента) =====\n  var leafVals = [];\n  (function collect(n) {\n    if (n.children && n.children.length) n.children.forEach(collect);\n    else if (n !== root) leafVals.push(n.value);\n  })(root);\n  var minV = leafVals.length ? Math.min.apply(null, leafVals) : 0;\n  var maxV = leafVals.length ? Math.max.apply(null, leafVals) : 1;\n\n  // ===== Палитра верхнего уровня =====\n  var palette = (w.colors && w.colors.length) ? w.colors\n    : ['#4f9dff', '#f5a623', '#7ed321', '#bd10e0', '#50e3c2', '#e0457b', '#9013fe'];\n\n  // ===== Преобразование в данные ECharts с раскраской =====\n  function convert(node, depth, baseColor) {\n    return (node.children || []).map(function (c, idx) {\n      var isLeaf = !c.children || !c.children.length;\n      var thisBase = depth === 0 ? palette[idx % palette.length] : baseColor;\n      var color;\n      var ruleColor = applyRules(c.value);\n      if (ruleColor) color = ruleColor;\n      else if (c._other) color = '#c0c0c0';\n      else if (gradientEnabled && isLeaf) {\n        var t = (maxV > minV) ? (c.value - minV) / (maxV - minV) : 0.5;\n        color = lerpColor(gradientColorLow, gradientColorHigh, t);\n      } else if (depth === 0) color = thisBase;\n      else color = lighten(thisBase, Math.min(0.55, 0.16 * depth));\n\n      var out = { name: c.name, value: c.value, itemStyle: { color: color } };\n      var kids = convert(c, depth + 1, thisBase);\n      if (kids.length) out.children = kids;\n      return out;\n    });\n  }\n  var sunData = convert(root, 0, null);\n  var topLevel = sunData.map(function (d) { return { name: d.name, color: d.itemStyle.color }; });\n\n  // ===== Метки =====\n  var rotateMap = { radial: 'radial', tangential: 'tangential', horizontal: 0 };\n  var rotateVal = rotateMap[labelRotate] !== undefined ? rotateMap[labelRotate] : 'radial';\n  var labelConfig = {\n    show: showLabels,\n    rotate: rotateVal,\n    align: labelAlign,\n    color: labelAutoColor ? 'inherit' : labelColor,\n    fontFamily: labelFontFamily,\n    fontSize: labelFontSize,\n    fontWeight: labelFontWeight,\n    minAngle: 6,\n    overflow: 'truncate',\n  };\n  if (labelStrokeWidth > 0) {\n    labelConfig.textBorderColor = labelStrokeColor;\n    labelConfig.textBorderWidth = labelStrokeWidth;\n  }\n\n  // ===== Подсказки =====\n  var rootTotal = root.value || 1;\n  var tooltipConfig = tooltipEnabled ? {\n    backgroundColor: 'rgba(255,255,255,0.98)',\n    borderColor: 'rgba(0,0,0,0.1)',\n    borderWidth: 1,\n    textStyle: { color: '#333', fontSize: 12 },\n    formatter: function (params) {\n      var path = (params.treePathInfo || []).slice(1).map(function (p) { return p.name; }).join(' / ');\n      var pct = params.value / rootTotal * 100;\n      return tooltipTemplate\n        .replace(/\\{name\\}/g, params.name)\n        .replace(/\\{value\\}/g, fmt(params.value))\n        .replace(/\\{percent\\}/g, pct.toFixed(1))\n        .replace(/\\{path\\}/g, path || params.name);\n    },\n  } : { show: false };\n\n  // ===== Центр с учётом легенды =====\n  var center = ['50%', '50%'];\n  if (showLegend) {\n    if (legendPosition === 'left') center = ['58%', '50%'];\n    else if (legendPosition === 'right') center = ['42%', '50%'];\n    else if (legendPosition.indexOf('top') === 0) center = ['50%', '56%'];\n    else if (legendPosition.indexOf('bottom') === 0) center = ['50%', '46%'];\n  }\n\n  // ===== Рендеринг =====\n  var existing = echarts.getInstanceByDom(container);\n  if (existing) existing.dispose();\n  container.style.position = 'relative';\n  var oldLeg = container.querySelector('.__sb_legend');\n  if (oldLeg) oldLeg.remove();\n\n  var chart = echarts.init(container);\n  chart.setOption({\n    backgroundColor: 'transparent',\n    animation: animationEnabled,\n    animationDuration: animationDuration,\n    animationDurationUpdate: animationDuration,\n    tooltip: tooltipConfig,\n    series: [{\n      type: 'sunburst',\n      radius: ['12%', '95%'],\n      center: center,\n      sort: undefined,\n      nodeClick: 'rootToNode',\n      emphasis: { focus: 'ancestor' },\n      label: labelConfig,\n      itemStyle: { borderColor: '#fff', borderWidth: 1 },\n      data: sunData,\n    }],\n  });\n\n  // ===== Легенда (HTML overlay): расположение + отключение =====\n  if (showLegend && topLevel.length) {\n    var legend = document.createElement('div');\n    legend.className = '__sb_legend';\n    legend.style.cssText =\n      'position:absolute;z-index:10;display:flex;flex-direction:column;gap:6px;' +\n      'padding:6px 8px;font-size:12px;font-family:' + labelFontFamily + ';' +\n      'background:rgba(255,255,255,0.6);border-radius:6px;pointer-events:auto;';\n\n    var posMap = {\n      'top-left': 'top:8px;left:8px;',\n      'top-right': 'top:8px;right:8px;',\n      'bottom-left': 'bottom:8px;left:8px;',\n      'bottom-right': 'bottom:8px;right:8px;',\n      'left': 'top:50%;left:8px;transform:translateY(-50%);',\n      'right': 'top:50%;right:8px;transform:translateY(-50%);',\n    };\n    legend.style.cssText += (posMap[legendPosition] || posMap['top-left']);\n\n    var active = {};\n    topLevel.forEach(function (t) {\n      var row = document.createElement('div');\n      row.style.cssText = 'display:flex;align-items:center;gap:6px;cursor:pointer;color:#333;';\n      row.innerHTML =\n        '<span style=\"display:inline-block;width:12px;height:12px;border-radius:2px;background:' + t.color + ';\"></span>' +\n        '<span>' + t.name + '</span>';\n      row.addEventListener('click', function () {\n        active[t.name] = !active[t.name];\n        row.style.opacity = active[t.name] ? '1' : '0.55';\n        chart.dispatchAction({ type: active[t.name] ? 'highlight' : 'downplay', seriesIndex: 0, name: t.name });\n      });\n      legend.appendChild(row);\n    });\n    container.appendChild(legend);\n  }\n\n  new ResizeObserver(function () { chart.resize(); }).observe(container);\n})();\n",
  "DataRoles": [
    {
      "Name": "category",
      "DisplayName": "Категория",
      "Description": "Иерархия (несколько строковых полей: уровень 1, 2, 3 ...)",
      "Kind": 0,
      "Optional": false
    },
    {
      "Name": "value",
      "DisplayName": "Значение",
      "Description": "Числовое поле — размер сектора",
      "Kind": 1,
      "Optional": false
    }
  ],
  "Properties": [
    {
      "DisplayName": "Глубина отображения (кол-во колец)",
      "PropertyName": "maxDepth",
      "Value": 3,
      "Type": 1
    },
    {
      "DisplayName": "Анимация",
      "PropertyName": "animationEnabled",
      "Value": true,
      "Type": 2
    },
    {
      "DisplayName": "Длительность анимации (мс)",
      "PropertyName": "animationDuration",
      "Value": 1000,
      "Type": 1
    },
    {
      "DisplayName": "Порог группировки в «Прочее» (%, 0=выкл)",
      "PropertyName": "otherThresholdPct",
      "Value": 3,
      "Type": 1
    },
    {
      "DisplayName": "Показывать метки секторов",
      "PropertyName": "showLabels",
      "Value": true,
      "Type": 2
    },
    {
      "DisplayName": "Авто-цвет меток (как у сектора)",
      "PropertyName": "labelAutoColor",
      "Value": true,
      "Type": 2
    },
    {
      "DisplayName": "Цвет меток (если авто выкл)",
      "PropertyName": "labelColor",
      "Value": "#ffffff",
      "Type": 3
    },
    {
      "DisplayName": "Цвет контура меток",
      "PropertyName": "labelStrokeColor",
      "Value": "#000000",
      "Type": 3
    },
    {
      "DisplayName": "Толщина контура меток",
      "PropertyName": "labelStrokeWidth",
      "Value": 2,
      "Type": 1
    },
    {
      "DisplayName": "Ориентация меток (radial/tangential/horizontal)",
      "PropertyName": "labelRotate",
      "Value": "radial",
      "Type": 0
    },
    {
      "DisplayName": "Выравнивание меток (left/center/right)",
      "PropertyName": "labelAlign",
      "Value": "center",
      "Type": 0
    },
    {
      "DisplayName": "Шрифт меток",
      "PropertyName": "labelFontFamily",
      "Value": "sans-serif",
      "Type": 0
    },
    {
      "DisplayName": "Размер шрифта меток",
      "PropertyName": "labelFontSize",
      "Value": 12,
      "Type": 1
    },
    {
      "DisplayName": "Насыщенность шрифта (normal/bold)",
      "PropertyName": "labelFontWeight",
      "Value": "normal",
      "Type": 0
    },
    {
      "DisplayName": "Включить подсказки",
      "PropertyName": "tooltipEnabled",
      "Value": true,
      "Type": 2
    },
    {
      "DisplayName": "Шаблон подсказки",
      "PropertyName": "tooltipTemplate",
      "Value": "{path}<br/>Значение: <b>{value}</b> ({percent}%)",
      "Type": 0
    },
    {
      "DisplayName": "Условное форматирование: градиент",
      "PropertyName": "gradientEnabled",
      "Value": false,
      "Type": 2
    },
    {
      "DisplayName": "Градиент: цвет минимума",
      "PropertyName": "gradientColorLow",
      "Value": "#cfe8ff",
      "Type": 3
    },
    {
      "DisplayName": "Градиент: цвет максимума",
      "PropertyName": "gradientColorHigh",
      "Value": "#08306b",
      "Type": 3
    },
    {
      "DisplayName": "Правила условий (JSON)",
      "PropertyName": "conditionRulesJson",
      "Value": "",
      "Type": 0
    },
    {
      "DisplayName": "Показывать легенду",
      "PropertyName": "showLegend",
      "Value": true,
      "Type": 2
    },
    {
      "DisplayName": "Расположение легенды",
      "PropertyName": "legendPosition",
      "Value": "top-left",
      "Type": 0
    }
  ],
  "PropertiesJson": ""
}