{
  "Name": "Bullet Chart",
  "Code": "(function() {\n    var widgetGuid = w.general.renderTo;\n    var container = document.getElementById(widgetGuid);\n    var props = w.props || {};\n    \n    // Отложенная инициализация для корректной работы при изменении данных\n    function initChart() {\n        try {\n            console.log('=== BULLET CHART DEBUG ===');\n            \n            // ============ ОЧИСТКА ПРЕДЫДУЩЕГО CHART ============\n            var existingChart = echarts.getInstanceByDom(container);\n            if (existingChart) {\n                existingChart.dispose();\n            }\n            \n            // Очищаем контейнер\n            container.innerHTML = '';\n            \n            // ============ ПРОВЕРКА ДАННЫХ ============\n            if (!w.data || !w.data.primaryData || !w.data.primaryData.items) {\n                showMessage('Нет данных. Привяжите поля.');\n                return;\n            }\n            \n            var items = w.data.primaryData.items;\n            \n            if (items.length === 0) {\n                showMessage('Пустой массив данных');\n                return;\n            }\n            \n            // ============ ОПРЕДЕЛЯЕМ ПОРЯДОК ПОЛЕЙ ИЗ COLS ============\n            var firstItem = items[0];\n            var cols = firstItem.cols || [];\n            \n            // cols = [\"Category\", \"Segment\", \"Actual\", \"Step\", \"Step_index\", \"Target\"]\n            // values соответствуют cols начиная с индекса 2 (после Category и Segment)\n            \n            // Создаём маппинг: имя поля -> индекс в values\n            var fieldToValueIdx = {};\n            for (var i = 2; i < cols.length; i++) {\n                fieldToValueIdx[cols[i]] = i - 2;\n            }\n            \n            console.log('Field mapping:', fieldToValueIdx);\n            // Должно быть: { Actual: 0, Step: 1, Step_index: 2, Target: 3 }\n            \n            var actualIdx = fieldToValueIdx['Actual'] !== undefined ? fieldToValueIdx['Actual'] : 0;\n            var stepIdx = fieldToValueIdx['Step'] !== undefined ? fieldToValueIdx['Step'] : 1;\n            var stepIndexIdx = fieldToValueIdx['Step_index'] !== undefined ? fieldToValueIdx['Step_index'] : 2;\n            var targetIdx = fieldToValueIdx['Target'] !== undefined ? fieldToValueIdx['Target'] : 3;\n            \n            console.log('Indices: Actual=' + actualIdx + ', Step=' + stepIdx + ', Step_index=' + stepIndexIdx + ', Target=' + targetIdx);\n            \n            // ============ НАСТРОЙКИ ============\n            var config = {\n                orientation: props.orientation || 'horizontal',\n                showLegend: props.showLegend !== false,\n                tooltipEnabled: props.tooltipEnabled !== false,\n                showCategoryLabels: props.showCategoryLabels !== false,\n                showTarget: props.showTarget !== false,\n                segmentColorMap: parseJsonSafe(props.segmentColorMapJson, {}),\n                actualColor: props.actualColor || '#1890ff',\n                targetColor: props.targetColor || '#333333',\n                actualBarWidthPct: props.actualBarWidthPct || 25,\n                rangeBarWidthPct: props.rangeBarWidthPct || 60,\n                targetMarkerThickness: props.targetMarkerThickness || 6,\n                targetMarkerLengthPct: props.targetMarkerLengthPct || 60,\n                axisStartAtZero: props.axisStartAtZero !== false,\n                axisMin: props.axisMin || 0,\n                axisMax: props.axisMax || 0,\n                axisMaxPaddingPct: props.axisMaxPaddingPct || 5,\n                fontSize: props.fontSize || 12\n            };\n            \n            function parseJsonSafe(str, defaultVal) {\n                if (!str) return defaultVal;\n                try { return JSON.parse(str); } \n                catch(e) { return defaultVal; }\n            }\n            \n            function showMessage(msg) {\n                container.innerHTML = '<div style=\"display:flex;align-items:center;justify-content:center;height:100%;color:#666;font-size:13px;text-align:center;padding:20px;\">' + msg + '</div>';\n            }\n            \n            // ============ ПАРСИНГ ДАННЫХ ============\n            var categoriesMap = {};\n            var allValues = [];\n            \n            items.forEach(function(item, idx) {\n                var keys = item.keys || [];\n                var values = item.values || [];\n                \n                var category = String(keys[0] || 'Category');\n                var segment = String(keys[1] || 'Segment');\n                \n                // Читаем значения по ПРАВИЛЬНЫМ индексам\n                var actual = values.length > actualIdx ? (values[actualIdx] || 0) : 0;\n                var step = values.length > stepIdx ? (values[stepIdx] || 0) : 0;\n                var stepIndex = values.length > stepIndexIdx ? (values[stepIndexIdx] || idx) : idx;\n                var target = values.length > targetIdx ? (values[targetIdx] || 0) : 0;\n                \n                console.log('Row ' + idx + ': category=' + category + ', segment=' + segment + \n                           ', actual=' + actual + ', step=' + step + ', stepIndex=' + stepIndex + ', target=' + target);\n                \n                if (!categoriesMap[category]) {\n                    categoriesMap[category] = {\n                        actual: actual,\n                        target: target,\n                        segments: []\n                    };\n                }\n                \n                categoriesMap[category].segments.push({\n                    name: segment,\n                    step: step,\n                    index: stepIndex\n                });\n                \n                allValues.push(actual, target);\n            });\n            \n            var categories = Object.keys(categoriesMap);\n            console.log('Категории:', categories);\n            \n            if (categories.length === 0) {\n                showMessage('Нет категорий');\n                return;\n            }\n            \n            // Сортировка сегментов по step_index\n            categories.forEach(function(cat) {\n                var segs = categoriesMap[cat].segments;\n                segs.sort(function(a, b) { return a.index - b.index; });\n                \n                var cumulative = 0;\n                segs.forEach(function(seg) {\n                    cumulative += seg.step;\n                    seg.end = cumulative;\n                    allValues.push(cumulative);\n                });\n            });\n            \n            // ============ ОСИ ============\n            var validValues = allValues.filter(function(v) { return typeof v === 'number' && !isNaN(v) && v > 0; });\n            var dataMax = validValues.length > 0 ? Math.max.apply(null, validValues) : 100;\n            \n            var axisMin = config.axisMin || 0;\n            var axisMax = config.axisMax || (dataMax * 1.1);\n            \n            // ============ ЦВЕТА ============\n            function getSegmentColor(name, index) {\n                if (config.segmentColorMap && config.segmentColorMap[name]) {\n                    return config.segmentColorMap[name];\n                }\n                var lowerName = name.toLowerCase();\n                if (lowerName === 'low') return 'rgba(76, 175, 80, 0.45)';\n                if (lowerName === 'medium') return 'rgba(255, 193, 7, 0.55)';\n                if (lowerName === 'high') return 'rgba(244, 67, 54, 0.45)';\n                \n                var defaultColors = ['rgba(200,200,200,0.4)', 'rgba(180,180,180,0.5)', 'rgba(160,160,160,0.6)'];\n                return defaultColors[index % defaultColors.length];\n            }\n            \n            // ============ СЕРИИ ============\n            var series = [];\n            var isHorizontal = config.orientation === 'horizontal';\n            \n            // Уникальные сегменты в правильном порядке\n            var segmentOrder = {};\n            categories.forEach(function(cat) {\n                categoriesMap[cat].segments.forEach(function(seg) {\n                    if (!segmentOrder.hasOwnProperty(seg.name)) {\n                        segmentOrder[seg.name] = seg.index;\n                    }\n                });\n            });\n            \n            var uniqueSegments = Object.keys(segmentOrder).sort(function(a, b) {\n                return segmentOrder[a] - segmentOrder[b];\n            });\n            \n            console.log('Сегменты (сортированные):', uniqueSegments);\n            \n            // Фоновые диапазоны (стек)\n            uniqueSegments.forEach(function(segName, segIdx) {\n                var segData = categories.map(function(cat) {\n                    var found = categoriesMap[cat].segments.find(function(s) { return s.name === segName; });\n                    return found ? found.step : 0;\n                });\n                \n                series.push({\n                    name: segName,\n                    type: 'bar',\n                    stack: 'range',\n                    barWidth: config.rangeBarWidthPct + '%',\n                    itemStyle: { color: getSegmentColor(segName, segIdx) },\n                    emphasis: { disabled: true },\n                    data: segData,\n                    z: 1\n                });\n            });\n            \n            // Actual bar — custom серия для точного центрирования\n            series.push({\n                name: 'Actual',\n                type: 'custom',\n                itemStyle: { color: config.actualColor },\n                renderItem: function(params, api) {\n                    var catIndex = params.dataIndex;\n                    var actualVal = api.value(0);\n                    if (!actualVal || actualVal <= 0) return null;\n                    \n                    if (isHorizontal) {\n                        var start = api.coord([0, catIndex]);\n                        var end = api.coord([actualVal, catIndex]);\n                        var bandSize = api.size([0, 1]);\n                        var barHeight = bandSize[1] * config.actualBarWidthPct / 100;\n                        return {\n                            type: 'rect',\n                            shape: {\n                                x: start[0],\n                                y: start[1] - barHeight / 2,\n                                width: end[0] - start[0],\n                                height: barHeight,\n                                r: 2\n                            },\n                            style: { fill: config.actualColor },\n                            z2: 10\n                        };\n                    } else {\n                        var startV = api.coord([catIndex, 0]);\n                        var endV = api.coord([catIndex, actualVal]);\n                        var bandSizeV = api.size([1, 0]);\n                        var barWidthV = bandSizeV[0] * config.actualBarWidthPct / 100;\n                        return {\n                            type: 'rect',\n                            shape: {\n                                x: startV[0] - barWidthV / 2,\n                                y: endV[1],\n                                width: barWidthV,\n                                height: startV[1] - endV[1],\n                                r: 2\n                            },\n                            style: { fill: config.actualColor },\n                            z2: 10\n                        };\n                    }\n                },\n                data: categories.map(function(cat) { return [categoriesMap[cat].actual]; }),\n                z: 10\n            });\n            \n            // Target marker\n            series.push({\n                name: 'Target',\n                type: 'custom',\n                renderItem: function(params, api) {\n                    try {\n                        if (!config.showTarget) return null;\n                        \n                        var catIndex = params.dataIndex;\n                        var cat = categories[catIndex];\n                        if (!cat || !categoriesMap[cat]) return null;\n                        \n                        var targetVal = categoriesMap[cat].target;\n                        if (!targetVal || targetVal <= 0) return null;\n                        \n                        var halfLen = config.targetMarkerLengthPct / 2;\n                        var coord = isHorizontal ? api.coord([targetVal, catIndex]) : api.coord([catIndex, targetVal]);\n                        var size = isHorizontal ? api.size([0, 1]) : api.size([1, 0]);\n                        \n                        if (!coord || !size) return null;\n                        \n                        if (isHorizontal) {\n                            var h = size[1] * halfLen / 100;\n                            return {\n                                type: 'line',\n                                shape: { x1: coord[0], y1: coord[1] - h, x2: coord[0], y2: coord[1] + h },\n                                style: { stroke: config.targetColor, lineWidth: config.targetMarkerThickness },\n                                z2: 100\n                            };\n                        } else {\n                            var wt = size[0] * halfLen / 100;\n                            return {\n                                type: 'line',\n                                shape: { x1: coord[0] - wt, y1: coord[1], x2: coord[0] + wt, y2: coord[1] },\n                                style: { stroke: config.targetColor, lineWidth: config.targetMarkerThickness },\n                                z2: 100\n                            };\n                        }\n                    } catch(e) { return null; }\n                },\n                data: categories.map(function() { return 0; }),\n                z: 20\n            });\n            \n            // ============ ОПЦИИ ============\n            var legendData = uniqueSegments.concat(['Actual']);\n            \n            var option = {\n                tooltip: {\n                    show: config.tooltipEnabled,\n                    trigger: 'axis',\n                    axisPointer: { type: 'shadow' },\n                    formatter: function(params) {\n                        if (!params || !params.length) return '';\n                        var cat = categories[params[0].dataIndex];\n                        var d = categoriesMap[cat];\n                        if (!d) return '';\n                        \n                        var html = '<b>' + cat + '</b><br/>';\n                        html += 'Факт: <b style=\"color:' + config.actualColor + '\">' + d.actual + '</b><br/>';\n                        html += 'План: <b>' + d.target + '</b>';\n                        return html;\n                    }\n                },\n                legend: {\n                    show: config.showLegend,\n                    data: legendData,\n                    bottom: 5\n                },\n                grid: { left: '12%', right: '8%', top: '8%', bottom: config.showLegend ? '18%' : '8%', containLabel: true },\n                xAxis: isHorizontal \n                    ? { type: 'value', min: axisMin, max: axisMax }\n                    : { type: 'category', data: categories, show: config.showCategoryLabels },\n                yAxis: isHorizontal \n                    ? { type: 'category', data: categories, show: config.showCategoryLabels }\n                    : { type: 'value', min: axisMin, max: axisMax },\n                series: series\n            };\n            \n            console.log('ECharts option:', option);\n            \n            // ============ РЕНДЕР ============\n            // Проверяем размеры контейнера\n            var rect = container.getBoundingClientRect();\n            console.log('Container size:', rect.width, 'x', rect.height);\n            \n            if (rect.width === 0 || rect.height === 0) {\n                console.warn('Container has zero size, retrying...');\n                setTimeout(initChart, 100);\n                return;\n            }\n            \n            var chart = echarts.init(container);\n            chart.setOption(option);\n            \n            // ResizeObserver\n            if (window.ResizeObserver) {\n                new ResizeObserver(function() { \n                    try { chart.resize(); } catch(e) {}\n                }).observe(container);\n            }\n            \n            console.log('=== BULLET CHART READY ===');\n            \n        } catch(error) {\n            console.error('BULLET CHART ERROR:', error);\n            container.innerHTML = '<div style=\"color:red;padding:20px;\">Ошибка: ' + error.message + '</div>';\n        }\n    }\n    \n    // Запускаем с небольшой задержкой для корректной работы при изменении данных\n    setTimeout(initChart, 50);\n    \n})();",
  "DataRoles": [
    {
      "Name": "8eeb235b768d4a7fb0865a95ac180832",
      "DisplayName": "Категория",
      "Description": "",
      "Kind": 0,
      "Type": 0,
      "Optional": false
    },
    {
      "Name": "a76a80ca007041b2bf31fe3e297c7c4e",
      "DisplayName": "Значение",
      "Description": "",
      "Kind": 1,
      "Type": 0,
      "Optional": false
    }
  ],
  "Properties": [
    {
      "Value": "horizontal",
      "Type": 0,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Ориентация",
      "PropertyName": "orientation"
    },
    {
      "Value": true,
      "Type": 2,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Показывать легенду\t",
      "PropertyName": "showLegend"
    },
    {
      "Value": true,
      "Type": 2,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Всплывающая подсказка\t",
      "PropertyName": "tooltipEnabled"
    },
    {
      "Value": true,
      "Type": 2,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Показывать подписи категорий\t",
      "PropertyName": "showCategoryLabels"
    },
    {
      "Value": true,
      "Type": 2,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Показывать Target\t",
      "PropertyName": "showTarget"
    },
    {
      "Value": "{\"Low\": \"rgba(144, 238, 144, 0.5)\", \"Medium\": \"rgba(255, 255, 150, 0.5)\", \"High\": \"rgba(255, 182, 193, 0.5)\"}",
      "Type": 0,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Градиент сегментов (JSON)\t",
      "PropertyName": "segmentColorMapJson"
    },
    {
      "Value": "rgb(223,42,42)",
      "Type": 3,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Цвет Actual\t",
      "PropertyName": "actualColor"
    },
    {
      "Value": "rgb(251,163,163)",
      "Type": 3,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Цвет Target\t",
      "PropertyName": "targetColor"
    },
    {
      "Value": 25,
      "Type": 1,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Ширина Actual, %\t",
      "PropertyName": "actualBarWidthPct"
    },
    {
      "Value": 60,
      "Type": 1,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Ширина фона, %\t",
      "PropertyName": "rangeBarWidthPct"
    },
    {
      "Value": 6,
      "Type": 1,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Толщина Target, px\t",
      "PropertyName": "targetMarkerThickness"
    },
    {
      "Value": 60,
      "Type": 1,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Длина Target, %\t",
      "PropertyName": "targetMarkerLengthPct"
    },
    {
      "Value": true,
      "Type": 2,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Ось с нуля\t",
      "PropertyName": "axisStartAtZero"
    },
    {
      "Value": 0,
      "Type": 1,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Минимум оси\t",
      "PropertyName": "axisMin"
    },
    {
      "Value": 0,
      "Type": 1,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Максимум оси\t",
      "PropertyName": "axisMax"
    },
    {
      "Value": 5,
      "Type": 1,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Запас сверху, %\t",
      "PropertyName": "axisMaxPaddingPct"
    },
    {
      "Value": 12,
      "Type": 1,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Размер шрифта\t",
      "PropertyName": "fontSize"
    },
    {
      "Value": "center",
      "Type": 0,
      "ElementType": "UserWidgetProperty",
      "DisplayName": "Выравнивание подписей\t",
      "PropertyName": "categoryLabelAlign"
    }
  ],
  "PropertiesJson": ""
}