{
  "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 maxLinks = props.maxLinks != null ? Number(props.maxLinks) : 50;\n  var layout = (props.layout || 'force').toLowerCase();\n  var showLegend = props.showLegend !== false;\n  var legendPosition = props.legendPosition || 'top-center';\n\n  var lineColor = props.lineColor || '#9aa0a6';\n  var lineWidth = props.lineWidth != null ? Number(props.lineWidth) : 1.5;\n  var lineType = (props.lineType || 'solid').toLowerCase();\n  var lineCurveness = props.lineCurveness != null ? Number(props.lineCurveness) : 0.1;\n\n  var edgeLength = props.edgeLength != null ? Number(props.edgeLength) : 120;\n  var repulsion = props.repulsion != null ? Number(props.repulsion) : 320;\n\n  var showLabels = props.showLabels !== false;\n  var labelPosition = (props.labelPosition || 'right').toLowerCase();\n  var labelOrientation = (props.labelOrientation || 'parallel').toLowerCase();\n  var labelColor = props.labelColor || '#e8eaed';\n  var labelStrokeColor = props.labelStrokeColor || '#000000';\n  var labelStrokeWidth = props.labelStrokeWidth != null ? Number(props.labelStrokeWidth) : 2;\n\n  var autoRound = props.autoRound !== false;\n  var roundDecimals = props.roundDecimals != null ? Number(props.roundDecimals) : 1;\n\n  var tooltipEnabled = props.tooltipEnabled !== false;\n  var tooltipTemplate = props.tooltipTemplate || '{source} → {target}<br/>Вес: <b>{value}</b>';\n\n  var nodeBaseSize = props.nodeBaseSize != null ? Number(props.nodeBaseSize) : 18;\n\n  // ===== Авто-округление (одновременно млрд / млн / тыс) =====\n  function trimZeros(numStr) {\n    if (numStr.indexOf('.') === -1) return numStr;\n    return numStr.replace(/\\.?0+$/, '');\n  }\n  function formatNumber(value) {\n    if (value == null || isNaN(value)) return '0';\n    if (!autoRound) {\n      return trimZeros(Number(value).toFixed(roundDecimals));\n    }\n    var abs = Math.abs(value);\n    var out, suffix;\n    if (abs >= 1e9) {\n      out = value / 1e9;\n      suffix = ' млрд';\n    } else if (abs >= 1e6) {\n      out = value / 1e6;\n      suffix = ' млн';\n    } else if (abs >= 1e3) {\n      out = value / 1e3;\n      suffix = ' тыс';\n    } else {\n      return trimZeros(Number(value).toFixed(roundDecimals));\n    }\n    return trimZeros(out.toFixed(roundDecimals)) + suffix;\n  }\n\n  // ===== Подготовка связей с ограничением вывода =====\n  var rawLinks = items.map(function (item) {\n    return {\n      source: String(item.keys[0]),\n      target: String(item.keys[1]),\n      value: item.values && item.values.length ? Number(item.values[0]) || 0 : 0,\n    };\n  }).filter(function (l) {\n    return l.source && l.target;\n  });\n\n  // Ограничение на вывод данных по умолчанию: топ-N связей по весу\n  if (maxLinks > 0 && rawLinks.length > maxLinks) {\n    rawLinks = rawLinks\n      .slice()\n      .sort(function (a, b) { return b.value - a.value; })\n      .slice(0, maxLinks);\n  }\n\n  // ===== Построение узлов и классификация по категориям =====\n  var nodeMap = {};\n  function ensureNode(name) {\n    if (!nodeMap[name]) {\n      nodeMap[name] = { name: name, outW: 0, inW: 0, total: 0, out: 0, in: 0 };\n    }\n    return nodeMap[name];\n  }\n  rawLinks.forEach(function (l) {\n    var s = ensureNode(l.source);\n    var t = ensureNode(l.target);\n    s.outW += l.value; s.out += 1;\n    t.inW += l.value; t.in += 1;\n    s.total += l.value;\n    t.total += l.value;\n  });\n\n  var categories = [\n    { name: 'Источник' },\n    { name: 'Промежуточный' },\n    { name: 'Конечный' },\n  ];\n  function categoryIndex(node) {\n    if (node.out > 0 && node.in === 0) return 0; // только исходящие\n    if (node.out > 0 && node.in > 0) return 1;   // и входящие, и исходящие\n    return 2;                                    // только входящие\n  }\n\n  var totals = Object.keys(nodeMap).map(function (k) { return nodeMap[k].total; });\n  var maxTotal = Math.max.apply(null, totals.concat([1]));\n\n  var nodes = Object.keys(nodeMap).map(function (k) {\n    var n = nodeMap[k];\n    var size = nodeBaseSize + (n.total / maxTotal) * nodeBaseSize * 2.5;\n    return {\n      id: k,\n      name: k,\n      value: n.total,\n      symbolSize: size,\n      category: categoryIndex(n),\n      _meta: n,\n    };\n  });\n\n  // ===== Стиль соединительных линий =====\n  var dashMap = {\n    solid: undefined,\n    dashed: [8, 6],\n    dotted: [2, 4],\n  };\n  var lineDash = dashMap[lineType];\n\n  var links = rawLinks.map(function (l) {\n    return {\n      source: l.source,\n      target: l.target,\n      value: l.value,\n      lineStyle: {\n        color: lineColor,\n        width: lineWidth + (l.value / maxTotal) * lineWidth * 2,\n        type: lineDash || 'solid',\n        curveness: lineCurveness,\n      },\n    };\n  });\n\n  // ===== Метки значений: позиция + ориентация (параллельно/вертикально) + контур =====\n  var labelRotate = labelOrientation === 'vertical' ? 90 : 0;\n  var labelConfig = {\n    show: showLabels,\n    position: labelPosition,\n    rotate: labelRotate,\n    color: labelColor,\n    fontSize: 12,\n    fontWeight: 500,\n    formatter: function (p) {\n      return p.data.name + ' (' + formatNumber(p.data.value) + ')';\n    },\n  };\n  if (labelStrokeWidth > 0) {\n    labelConfig.textBorderColor = labelStrokeColor;\n    labelConfig.textBorderWidth = labelStrokeWidth;\n  }\n\n  // ===== Расположение легенды =====\n  function buildLegend() {\n    if (!showLegend) return { show: false };\n    var pos = legendPosition;\n    var cfg = {\n      show: true,\n      data: categories.map(function (c) { return c.name; }),\n      textStyle: { color: '#9aa0a6', fontSize: 12 },\n      itemGap: 16,\n      icon: 'circle',\n    };\n    // Вертикальные позиции слева/справа\n    if (pos === 'left' || pos === 'right') {\n      cfg.orient = 'vertical';\n      cfg[pos] = 10;\n      cfg.top = 'middle';\n      return cfg;\n    }\n    cfg.orient = 'horizontal';\n    var parts = pos.split('-');\n    var vert = parts[0] || 'top';\n    var horiz = parts[1] || 'center';\n    cfg[vert] = 8;\n    if (horiz === 'left') cfg.left = 12;\n    else if (horiz === 'right') cfg.right = 12;\n    else cfg.left = 'center';\n    return cfg;\n  }\n\n  // ===== Подсказки при наведении (настраиваемые) =====\n  var tooltipConfig = tooltipEnabled\n    ? {\n        trigger: 'item',\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 (p) {\n          if (p.dataType === 'edge') {\n            return tooltipTemplate\n              .replace(/\\{source\\}/g, p.data.source)\n              .replace(/\\{target\\}/g, p.data.target)\n              .replace(/\\{value\\}/g, formatNumber(p.data.value));\n          }\n          var m = p.data._meta || {};\n          var catName = categories[p.data.category].name;\n          return (\n            '<div style=\"font-weight:600;margin-bottom:6px;border-bottom:1px solid #eee;padding-bottom:4px;\">' +\n            p.data.name +\n            '</div>' +\n            '<div style=\"margin:3px 0;color:#666;\">Тип: <b style=\"color:#333;\">' + catName + '</b></div>' +\n            '<div style=\"margin:3px 0;color:#666;\">Связей: <b style=\"color:#333;\">' + ((m.out || 0) + (m.in || 0)) + '</b></div>' +\n            '<div style=\"margin:3px 0;color:#666;\">Суммарный вес: <b style=\"color:#333;\">' + formatNumber(p.data.value) + '</b></div>'\n          );\n        },\n      }\n    : { show: false };\n\n  // ===== Раскладка =====\n  var seriesLayout =\n    layout === 'circular'\n      ? { layout: 'circular', circular: { rotateLabel: true } }\n      : {\n          layout: 'force',\n          force: {\n            repulsion: repulsion,\n            edgeLength: edgeLength,\n            gravity: 0.08,\n          },\n        };\n\n  // ===== Сборка графика =====\n  var existing = echarts.getInstanceByDom(container);\n  if (existing) existing.dispose();\n  var chart = echarts.init(container);\n\n  var palette = w.colors && w.colors.length ? w.colors : ['#4f9dff', '#f5a623', '#7ed321'];\n  categories.forEach(function (c, i) {\n    c.itemStyle = { color: palette[i % palette.length] };\n  });\n\n  var topPad = showLegend && legendPosition.indexOf('top') === 0 ? 40 : 16;\n  var bottomPad = showLegend && legendPosition.indexOf('bottom') === 0 ? 36 : 16;\n\n  var option = {\n    backgroundColor: 'transparent',\n    tooltip: tooltipConfig,\n    legend: buildLegend(),\n    grid: { top: topPad, bottom: bottomPad },\n    series: [\n      Object.assign(\n        {\n          type: 'graph',\n          roam: true,\n          draggable: true,\n          edgeSymbol: ['none', 'arrow'],\n          edgeSymbolSize: [0, 8],\n          categories: categories,\n          label: labelConfig,\n          lineStyle: { opacity: 0.85 },\n          emphasis: {\n            focus: 'adjacency',\n            lineStyle: { width: lineWidth + 2 },\n            label: { show: true },\n          },\n          data: nodes,\n          links: links,\n          top: topPad,\n          bottom: bottomPad,\n        },\n        seriesLayout\n      ),\n    ],\n  };\n\n  chart.setOption(option);\n\n  new ResizeObserver(function () {\n    chart.resize();\n  }).observe(container);\n})();\n",
  "DataRoles": [
    {
      "Name": "category",
      "DisplayName": "Категория",
      "Description": "Два строковых поля: Источник и Цель связи (функции)",
      "Kind": 0,
      "Optional": false
    },
    {
      "Name": "value",
      "DisplayName": "Значение",
      "Description": "Числовое поле — вес/сила связи",
      "Kind": 1,
      "Optional": true
    }
  ],
  "Properties": [
    {
      "DisplayName": "Макс. число связей (0=все)",
      "PropertyName": "maxLinks",
      "Value": 50,
      "Type": 1
    },
    {
      "DisplayName": "Раскладка (force/circular)",
      "PropertyName": "layout",
      "Value": "force",
      "Type": 0
    },
    {
      "DisplayName": "Показывать легенду",
      "PropertyName": "showLegend",
      "Value": true,
      "Type": 2
    },
    {
      "DisplayName": "Расположение легенды",
      "PropertyName": "legendPosition",
      "Value": "top-center",
      "Type": 0
    },
    {
      "DisplayName": "Цвет соединительных линий",
      "PropertyName": "lineColor",
      "Value": "#9aa0a6",
      "Type": 3
    },
    {
      "DisplayName": "Толщина линий",
      "PropertyName": "lineWidth",
      "Value": 1.5,
      "Type": 1
    },
    {
      "DisplayName": "Формат линий (solid/dashed/dotted)",
      "PropertyName": "lineType",
      "Value": "solid",
      "Type": 0
    },
    {
      "DisplayName": "Изгиб линий (0-1)",
      "PropertyName": "lineCurveness",
      "Value": 0.1,
      "Type": 1
    },
    {
      "DisplayName": "Расстояние между узлами (edgeLength)",
      "PropertyName": "edgeLength",
      "Value": 120,
      "Type": 1
    },
    {
      "DisplayName": "Сила отталкивания (repulsion)",
      "PropertyName": "repulsion",
      "Value": 320,
      "Type": 1
    },
    {
      "DisplayName": "Показывать метки значений",
      "PropertyName": "showLabels",
      "Value": true,
      "Type": 2
    },
    {
      "DisplayName": "Позиция меток (inside/top/bottom/left/right)",
      "PropertyName": "labelPosition",
      "Value": "right",
      "Type": 0
    },
    {
      "DisplayName": "Ориентация меток (parallel/vertical)",
      "PropertyName": "labelOrientation",
      "Value": "parallel",
      "Type": 0
    },
    {
      "DisplayName": "Цвет меток",
      "PropertyName": "labelColor",
      "Value": "#e8eaed",
      "Type": 3
    },
    {
      "DisplayName": "Цвет контура меток",
      "PropertyName": "labelStrokeColor",
      "Value": "#000000",
      "Type": 3
    },
    {
      "DisplayName": "Толщина контура меток",
      "PropertyName": "labelStrokeWidth",
      "Value": 2,
      "Type": 1
    },
    {
      "DisplayName": "Авто-округление (млрд/млн/тыс)",
      "PropertyName": "autoRound",
      "Value": true,
      "Type": 2
    },
    {
      "DisplayName": "Знаков после запятой",
      "PropertyName": "roundDecimals",
      "Value": 1,
      "Type": 1
    },
    {
      "DisplayName": "Включить подсказки",
      "PropertyName": "tooltipEnabled",
      "Value": true,
      "Type": 2
    },
    {
      "DisplayName": "Шаблон подсказки связи",
      "PropertyName": "tooltipTemplate",
      "Value": "{source} → {target}<br/>Вес: <b>{value}</b>",
      "Type": 0
    },
    {
      "DisplayName": "Базовый размер узла",
      "PropertyName": "nodeBaseSize",
      "Value": 18,
      "Type": 1
    }
  ],
  "PropertiesJson": ""
}