{
  "Name": "Hierarchical Row with Integrated Bar — Standard",
  "Code": "(function() {\r\n  var container = document.getElementById(w.general.renderTo);\r\n  var props = w.props || {};\r\n  var items = w.data?.primaryData?.items || [];\r\n\r\n  // === Default props ===\r\n  var showSubcategory = props.showSubcategory !== false;\r\n  var expandByDefault = props.expandByDefault === true;\r\n  var showDeltaAbsolute = props.showDeltaAbsolute !== false;\r\n  var showDeltaPercent = props.showDeltaPercent !== false;\r\n  var showCategoryTotals = props.showCategoryTotals !== false;\r\n  var showTotalRow = props.showTotalRow !== false;\r\n  var positiveDeltaColor = props.positiveDeltaColor || '#2e7d32';\r\n  var negativeDeltaColor = props.negativeDeltaColor || '#c62828';\r\n  var fontSize = props.fontSize || 12;\r\n  var fontFamily = props.fontFamily || 'Roboto';\r\n  var textColor = props.textColor || '#333333';\r\n  var headerColor = props.headerColor || '#666666';\r\n  var borderColor = props.borderColor || '#e0e0e0';\r\n  var categoryBgColor = props.categoryBgColor || '#f5f5f5';\r\n  var categoryTextColor = props.categoryTextColor || '#333333';\r\n  var subcategoryBgColor = props.subcategoryBgColor || '#ffffff';\r\n  var subcategoryTextColor = props.subcategoryTextColor || '#333333';\r\n  var maxBarWidth = props.maxBarWidth || 80;\r\n\r\n  // Column widths for consistent alignment\r\n  var colWidthName = 120;\r\n  var colWidthValue = 85;\r\n  var colWidthDelta = 85;\r\n  var colWidthBar = maxBarWidth * 2 + 4; // Double width for left+right bars with center line\r\n  var colWidthPct = 55;\r\n\r\n  if (!items.length) {\r\n    container.innerHTML = '<div style=\"display:flex;align-items:center;justify-content:center;height:100%;color:#999;font-family:' + fontFamily + ';\">Нет данных</div>';\r\n    return;\r\n  }\r\n\r\n  // === Parse data and determine current/previous year ===\r\n  var allYears = new Set();\r\n  items.forEach(function(item) {\r\n    var dateStr = item.keys[2] || '';\r\n    var year = parseInt(dateStr.substring(0, 4));\r\n    if (!isNaN(year)) {\r\n      allYears.add(year);\r\n    }\r\n  });\r\n  \r\n  var yearsArray = Array.from(allYears).sort();\r\n  var currentYear = yearsArray[yearsArray.length - 1];\r\n  var previousYear = yearsArray[yearsArray.length - 2];\r\n\r\n  // === Build hierarchical data structure ===\r\n  var dataMap = {};\r\n  var categoriesOrder = [];\r\n  var categoriesSet = new Set();\r\n\r\n  items.forEach(function(item) {\r\n    var category = item.keys[0];\r\n    var subcategory = item.keys[1] || '';\r\n    var dateStr = item.keys[2] || '';\r\n    var value = item.values[0] || 0;\r\n    \r\n    var year = parseInt(dateStr.substring(0, 4));\r\n    var period = dateStr.substring(5);\r\n    \r\n    if (!categoriesSet.has(category)) {\r\n      categoriesSet.add(category);\r\n      categoriesOrder.push(category);\r\n    }\r\n    \r\n    if (!dataMap[category]) {\r\n      dataMap[category] = {\r\n        subcategories: {},\r\n        subcategoriesOrder: []\r\n      };\r\n    }\r\n    \r\n    var catData = dataMap[category];\r\n    \r\n    if (!catData.subcategories[subcategory]) {\r\n      catData.subcategories[subcategory] = {\r\n        periods: {}\r\n      };\r\n      catData.subcategoriesOrder.push(subcategory);\r\n    }\r\n    \r\n    var subData = catData.subcategories[subcategory];\r\n    \r\n    if (!subData.periods[period]) {\r\n      subData.periods[period] = { ac: 0, py: 0 };\r\n    }\r\n    \r\n    if (year === currentYear) {\r\n      subData.periods[period].ac = value;\r\n    } else if (year === previousYear) {\r\n      subData.periods[period].py = value;\r\n    }\r\n  });\r\n\r\n  // === Calculate aggregated values ===\r\n  var hierarchicalData = [];\r\n  var maxDeltaAbs = 0;\r\n  var maxDeltaPct = 0;\r\n\r\n  categoriesOrder.forEach(function(category) {\r\n    var catData = dataMap[category];\r\n    var categoryRow = {\r\n      type: 'category',\r\n      name: category,\r\n      py: 0,\r\n      ac: 0,\r\n      delta: 0,\r\n      deltaPct: null,\r\n      expanded: expandByDefault,\r\n      children: []\r\n    };\r\n    \r\n    catData.subcategoriesOrder.forEach(function(subcategory) {\r\n      var subData = catData.subcategories[subcategory];\r\n      var subPy = 0;\r\n      var subAc = 0;\r\n      \r\n      Object.keys(subData.periods).forEach(function(period) {\r\n        var periodData = subData.periods[period];\r\n        subPy += periodData.py;\r\n        subAc += periodData.ac;\r\n      });\r\n      \r\n      var subDelta = subAc - subPy;\r\n      var subDeltaPct = subPy !== 0 ? ((subAc - subPy) / subPy) * 100 : null;\r\n      \r\n      categoryRow.py += subPy;\r\n      categoryRow.ac += subAc;\r\n      \r\n      maxDeltaAbs = Math.max(maxDeltaAbs, Math.abs(subDelta));\r\n      if (subDeltaPct !== null) {\r\n        maxDeltaPct = Math.max(maxDeltaPct, Math.abs(subDeltaPct));\r\n      }\r\n      \r\n      categoryRow.children.push({\r\n        type: 'subcategory',\r\n        name: subcategory,\r\n        py: subPy,\r\n        ac: subAc,\r\n        delta: subDelta,\r\n        deltaPct: subDeltaPct\r\n      });\r\n    });\r\n    \r\n    categoryRow.delta = categoryRow.ac - categoryRow.py;\r\n    categoryRow.deltaPct = categoryRow.py !== 0 ? ((categoryRow.ac - categoryRow.py) / categoryRow.py) * 100 : null;\r\n    \r\n    maxDeltaAbs = Math.max(maxDeltaAbs, Math.abs(categoryRow.delta));\r\n    if (categoryRow.deltaPct !== null) {\r\n      maxDeltaPct = Math.max(maxDeltaPct, Math.abs(categoryRow.deltaPct));\r\n    }\r\n    \r\n    hierarchicalData.push(categoryRow);\r\n  });\r\n\r\n  // Calculate total row\r\n  var totalRow = {\r\n    type: 'total',\r\n    name: 'Итог',\r\n    py: 0,\r\n    ac: 0,\r\n    delta: 0,\r\n    deltaPct: null\r\n  };\r\n  \r\n  hierarchicalData.forEach(function(cat) {\r\n    totalRow.py += cat.py;\r\n    totalRow.ac += cat.ac;\r\n  });\r\n  totalRow.delta = totalRow.ac - totalRow.py;\r\n  totalRow.deltaPct = totalRow.py !== 0 ? ((totalRow.ac - totalRow.py) / totalRow.py) * 100 : null;\r\n\r\n  // === Render functions ===\r\n  function formatNumber(num) {\r\n    if (num === null || num === undefined) return '';\r\n    return num.toLocaleString('ru-RU', { maximumFractionDigits: 0 });\r\n  }\r\n\r\n  // Render delta bar with center baseline (zero line)\r\n  function renderDeltaBar(delta, maxVal) {\r\n    var halfWidth = maxBarWidth;\r\n    var totalWidth = halfWidth * 2;\r\n    \r\n    // Container with center line\r\n    var containerStyle = 'position:relative;width:' + totalWidth + 'px;height:14px;display:flex;align-items:center;';\r\n    \r\n    // Center line (zero baseline)\r\n    var centerLineStyle = 'position:absolute;left:' + halfWidth + 'px;top:0;bottom:0;width:1px;background:#999;';\r\n    \r\n    var barHtml = '';\r\n    if (delta !== 0 && maxVal !== 0) {\r\n      var barWidth = Math.min(Math.abs(delta) / maxVal * halfWidth, halfWidth);\r\n      var color = delta >= 0 ? positiveDeltaColor : negativeDeltaColor;\r\n      \r\n      if (delta >= 0) {\r\n        // Positive: bar goes right from center\r\n        var barStyle = 'position:absolute;left:' + halfWidth + 'px;height:12px;width:' + barWidth + 'px;background:' + color + ';';\r\n        barHtml = '<div style=\"' + barStyle + '\"></div>';\r\n      } else {\r\n        // Negative: bar goes left from center\r\n        var leftPos = halfWidth - barWidth;\r\n        var barStyle = 'position:absolute;left:' + leftPos + 'px;height:12px;width:' + barWidth + 'px;background:' + color + ';';\r\n        barHtml = '<div style=\"' + barStyle + '\"></div>';\r\n      }\r\n    }\r\n    \r\n    return '<div style=\"' + containerStyle + '\">' +\r\n           '<div style=\"' + centerLineStyle + '\"></div>' +\r\n           barHtml +\r\n           '</div>';\r\n  }\r\n\r\n  function renderDeltaPercent(deltaPct) {\r\n    if (deltaPct === null) return '<div style=\"width:' + colWidthPct + 'px;\"></div>';\r\n    \r\n    var color = deltaPct >= 0 ? positiveDeltaColor : negativeDeltaColor;\r\n    var sign = deltaPct >= 0 ? '+' : '';\r\n    var value = sign + deltaPct.toFixed(0);\r\n    \r\n    var markerStyle = 'display:inline-flex;align-items:center;justify-content:flex-end;width:' + colWidthPct + 'px;';\r\n    var triangleStyle = 'width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;margin-right:3px;';\r\n    \r\n    if (deltaPct >= 0) {\r\n      triangleStyle += 'border-bottom:8px solid ' + color + ';';\r\n    } else {\r\n      triangleStyle += 'border-top:8px solid ' + color + ';';\r\n    }\r\n    \r\n    return '<div style=\"' + markerStyle + '\">' +\r\n           '<div style=\"' + triangleStyle + '\"></div>' +\r\n           '<span style=\"color:' + color + ';font-size:' + (fontSize - 1) + 'px;\">' + value + '</span>' +\r\n           '</div>';\r\n  }\r\n\r\n  function renderRow(row, isChild, isLast) {\r\n    var rowStyle = 'display:flex;align-items:center;padding:4px 8px;border-bottom:1px solid ' + borderColor + ';font-family:' + fontFamily + ',sans-serif;';\r\n    var nameStyle = 'flex:1;min-width:' + colWidthName + 'px;font-size:' + fontSize + 'px;color:' + textColor + ';white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';\r\n    var valueStyle = 'width:' + colWidthValue + 'px;text-align:right;font-size:' + fontSize + 'px;color:' + textColor + ';padding:0 8px;box-sizing:border-box;';\r\n    var deltaStyle = 'width:' + colWidthDelta + 'px;text-align:right;font-size:' + fontSize + 'px;padding:0 8px;box-sizing:border-box;';\r\n    var barCellStyle = 'width:' + colWidthBar + 'px;display:flex;align-items:center;justify-content:center;';\r\n    var pctCellStyle = 'width:' + colWidthPct + 'px;display:flex;align-items:center;justify-content:flex-end;';\r\n    \r\n    if (row.type === 'category') {\r\n      rowStyle += 'background:' + categoryBgColor + ';font-weight:600;';\r\n      nameStyle += 'font-weight:600;color:' + categoryTextColor + ';';\r\n      valueStyle = 'width:' + colWidthValue + 'px;text-align:right;font-size:' + fontSize + 'px;color:' + categoryTextColor + ';padding:0 8px;box-sizing:border-box;';\r\n    } else if (row.type === 'total') {\r\n      rowStyle += 'background:#e8e8e8;font-weight:700;border-top:2px solid ' + textColor + ';';\r\n      nameStyle += 'font-weight:700;';\r\n    } else if (isChild) {\r\n      rowStyle += 'background:' + subcategoryBgColor + ';';\r\n      nameStyle += 'padding-left:24px;color:' + subcategoryTextColor + ';';\r\n      valueStyle = 'width:' + colWidthValue + 'px;text-align:right;font-size:' + fontSize + 'px;color:' + subcategoryTextColor + ';padding:0 8px;box-sizing:border-box;';\r\n    }\r\n    \r\n    var expandIcon = '';\r\n    if (row.type === 'category' && showSubcategory && row.children && row.children.length > 0) {\r\n      var iconChar = row.expanded ? '▼' : '▶';\r\n      expandIcon = '<span class=\"expand-icon\" data-category=\"' + row.name + '\" style=\"cursor:pointer;margin-right:8px;font-size:10px;color:#666;\">' + iconChar + '</span>';\r\n    }\r\n\r\n    var html = '<div class=\"data-row\" style=\"' + rowStyle + '\">';\r\n    html += '<div style=\"' + nameStyle + '\">' + expandIcon + row.name + '</div>';\r\n    html += '<div style=\"' + valueStyle + '\">' + formatNumber(row.py) + '</div>';\r\n    html += '<div style=\"' + valueStyle + '\">' + formatNumber(row.ac) + '</div>';\r\n    \r\n    if (showDeltaAbsolute) {\r\n      var deltaColor = row.delta >= 0 ? positiveDeltaColor : negativeDeltaColor;\r\n      html += '<div style=\"' + deltaStyle + 'color:' + deltaColor + ';\">' + (row.delta >= 0 ? '+' : '') + formatNumber(row.delta) + '</div>';\r\n      html += '<div style=\"' + barCellStyle + '\">' + renderDeltaBar(row.delta, maxDeltaAbs) + '</div>';\r\n    }\r\n    \r\n    if (showDeltaPercent) {\r\n      html += '<div style=\"' + pctCellStyle + '\">' + renderDeltaPercent(row.deltaPct) + '</div>';\r\n    }\r\n    \r\n    html += '</div>';\r\n    return html;\r\n  }\r\n\r\n  // === Build the table ===\r\n  var tableHtml = '';\r\n  \r\n  // Header\r\n  var headerStyle = 'display:flex;align-items:center;padding:8px 8px;border-bottom:2px solid ' + textColor + ';background:#fafafa;font-family:' + fontFamily + ',sans-serif;';\r\n  var headerNameStyle = 'flex:1;min-width:' + colWidthName + 'px;font-size:' + fontSize + 'px;color:' + headerColor + ';font-weight:600;';\r\n  var headerValueStyle = 'width:' + colWidthValue + 'px;text-align:right;font-size:' + fontSize + 'px;color:' + headerColor + ';font-weight:600;padding:0 8px;box-sizing:border-box;';\r\n  var headerDeltaStyle = 'width:' + colWidthDelta + 'px;text-align:right;font-size:' + fontSize + 'px;color:' + headerColor + ';font-weight:600;padding:0 8px;box-sizing:border-box;';\r\n  var headerBarStyle = 'width:' + colWidthBar + 'px;text-align:center;font-size:' + fontSize + 'px;color:' + headerColor + ';font-weight:600;';\r\n  var headerPctStyle = 'width:' + colWidthPct + 'px;text-align:right;font-size:' + fontSize + 'px;color:' + headerColor + ';font-weight:600;';\r\n\r\n  tableHtml += '<div style=\"' + headerStyle + '\">';\r\n  tableHtml += '<div style=\"' + headerNameStyle + '\"></div>';\r\n  tableHtml += '<div style=\"' + headerValueStyle + '\">PY</div>';\r\n  tableHtml += '<div style=\"' + headerValueStyle + '\">AC</div>';\r\n  \r\n  if (showDeltaAbsolute) {\r\n    tableHtml += '<div style=\"' + headerDeltaStyle + '\">ΔPY</div>';\r\n    tableHtml += '<div style=\"' + headerBarStyle + '\"></div>';\r\n  }\r\n  \r\n  if (showDeltaPercent) {\r\n    tableHtml += '<div style=\"' + headerPctStyle + '\">ΔPY%</div>';\r\n  }\r\n  \r\n  tableHtml += '</div>';\r\n\r\n  // Data rows - Category FIRST, then subcategories below (раскрытие вниз)\r\n  hierarchicalData.forEach(function(category, catIdx) {\r\n    var isLastCategory = catIdx === hierarchicalData.length - 1;\r\n    \r\n    // First render category row\r\n    if (showCategoryTotals) {\r\n      tableHtml += renderRow(category, false, isLastCategory);\r\n    }\r\n    \r\n    // Then render subcategories below (if showSubcategory)\r\n    if (showSubcategory && category.children && category.children.length > 0) {\r\n      var childrenDisplay = category.expanded ? '' : 'display:none;';\r\n      tableHtml += '<div class=\"children-container\" data-parent=\"' + category.name + '\" style=\"' + childrenDisplay + '\">';\r\n      \r\n      category.children.forEach(function(child, childIdx) {\r\n        var isLastChild = childIdx === category.children.length - 1;\r\n        tableHtml += renderRow(child, true, isLastChild);\r\n      });\r\n      \r\n      tableHtml += '</div>';\r\n    }\r\n  });\r\n\r\n  // Total row (previously \"World\", now \"Итог\")\r\n  if (showTotalRow) {\r\n    tableHtml += renderRow(totalRow, false, true);\r\n  }\r\n\r\n  // === Render container ===\r\n  var wrapperStyle = 'font-family:' + fontFamily + ',sans-serif;height:100%;overflow:auto;';\r\n  container.innerHTML = '<div style=\"' + wrapperStyle + '\">' + tableHtml + '</div>';\r\n\r\n  // === Add interactivity ===\r\n  container.querySelectorAll('.expand-icon').forEach(function(icon) {\r\n    icon.addEventListener('click', function(e) {\r\n      var categoryName = e.target.getAttribute('data-category');\r\n      var childrenContainer = container.querySelector('.children-container[data-parent=\"' + categoryName + '\"]');\r\n      \r\n      if (childrenContainer) {\r\n        var isHidden = childrenContainer.style.display === 'none';\r\n        childrenContainer.style.display = isHidden ? '' : 'none';\r\n        e.target.textContent = isHidden ? '▼' : '▶';\r\n      }\r\n    });\r\n  });\r\n})();\r\n",
  "DataRoles": [
    {
      "Name": "category",
      "DisplayName": "Категория",
      "Description": "Строковые поля (Category, Subcategory, Date)",
      "Kind": 0,
      "Type": 0,
      "Optional": false
    },
    {
      "Name": "value",
      "DisplayName": "Значение",
      "Description": "Числовое поле (Value)",
      "Kind": 1,
      "Type": 0,
      "Optional": false
    }
  ],
  "Properties": [
    {
      "Value": true,
      "Type": 2,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Показать подкатегории",
      "PropertyName": "showSubcategory"
    },
    {
      "Value": false,
      "Type": 2,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Развернуть по умолчанию",
      "PropertyName": "expandByDefault"
    },
    {
      "Value": true,
      "Type": 2,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Показать ΔPY (абсолютное)",
      "PropertyName": "showDeltaAbsolute"
    },
    {
      "Value": true,
      "Type": 2,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Показать ΔPY% (процент)",
      "PropertyName": "showDeltaPercent"
    },
    {
      "Value": true,
      "Type": 2,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Показать итоги категорий",
      "PropertyName": "showCategoryTotals"
    },
    {
      "Value": true,
      "Type": 2,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Показать итоговую строку",
      "PropertyName": "showTotalRow"
    },
    {
      "Value": "#2e7d32",
      "Type": 3,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Цвет положительного отклонения",
      "PropertyName": "positiveDeltaColor"
    },
    {
      "Value": "#c62828",
      "Type": 3,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Цвет отрицательного отклонения",
      "PropertyName": "negativeDeltaColor"
    },
    {
      "Value": 12,
      "Type": 1,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Размер шрифта",
      "PropertyName": "fontSize"
    },
    {
      "Value": "Roboto",
      "Type": 0,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Шрифт",
      "PropertyName": "fontFamily"
    },
    {
      "Value": "#333333",
      "Type": 3,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Цвет текста",
      "PropertyName": "textColor"
    },
    {
      "Value": "#666666",
      "Type": 3,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Цвет заголовков",
      "PropertyName": "headerColor"
    },
    {
      "Value": "#e0e0e0",
      "Type": 3,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Цвет границ",
      "PropertyName": "borderColor"
    },
    {
      "Value": "#f5f5f5",
      "Type": 3,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Фон категорий",
      "PropertyName": "categoryBgColor"
    },
    {
      "Value": "#333333",
      "Type": 3,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Цвет текста категорий",
      "PropertyName": "categoryTextColor"
    },
    {
      "Value": "#ffffff",
      "Type": 3,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Фон подкатегорий",
      "PropertyName": "subcategoryBgColor"
    },
    {
      "Value": "#333333",
      "Type": 3,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Цвет текста подкатегорий",
      "PropertyName": "subcategoryTextColor"
    },
    {
      "Value": 80,
      "Type": 1,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Макс. ширина бара ΔPY (px)",
      "PropertyName": "maxBarWidth"
    }
  ],
  "PropertiesJson": ""
}