{% extends "base.html" %}
{% load static %}

{% block title %}宏观数据中心 - AgomTradePro{% endblock %}

{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/macro.css' %}">
{% endblock %}

{% block content %}
<div class="page-header">
    <div class="header-content">
        <div>
            <h1>宏观数据中心</h1>
            <p class="subtitle">中国宏观经济指标全景展示</p>
            <p class="description">
                指标名称、展示单位与说明统一来自 Data Center 指标目录与量纲规则。当前目录共 {{ stats.total_indicators }} 个指标，其中 {{ sync_supported_indicator_count }} 个已接入自动同步；
                无数据时“未同步”表示可抓取，“未接入”表示尚未接入自动同步链路。
            </p>
        </div>
        <div class="header-controls">
            <button id="refreshAllIndicatorsBtn" type="button" class="control-btn" onclick="refreshAllIndicators()">
                <span class="btn-icon">⟳</span> 全部刷新可抓取指标
            </button>
            <button class="control-btn" onclick="window.location.href='/macro/controller/'">
                <span class="btn-icon">⚙️</span> 数据管理
            </button>
        </div>
    </div>
</div>

{{ initial_history_rows|json_script:"initialHistoryRowsData" }}
<script>
const indicatorNamesMap = {};
const indicatorDescriptionsMap = {};
const indicatorUnitsMap = {};
const indicatorSeriesSemanticsMap = {};
const indicatorPairedCodesMap = {};
const indicatorChartPolicyMap = {};
const indicatorChartResetFrequencyMap = {};
const indicatorChartSegmentBasisMap = {};
const macroSeriesUrl = '{% url "api_data_center:dc-macro-series" %}';
const macroSyncUrl = '{% url "api_data_center:dc-sync-macro" %}';
const manualRefreshProviderId = Number('{{ refresh_provider_id|default:"0" }}');
const manualRefreshEndDate = '{{ refresh_end_date|date:"Y-m-d"|default:"" }}';
const bulkRefreshIndicatorCodes = {{ bulk_refresh_indicator_codes|safe }};
const DEFAULT_VISIBLE_POINTS = 36;
const RANGE_PRESET_OPTIONS = ['12', '24', '36', '60', 'all'];
const RESET_STACK_CHART_POLICIES = new Set(['yearly_reset_bar', 'reset_stack_bar']);
{% for category in indicator_categories %}
    {% for ind in category.indicators %}
        indicatorNamesMap['{{ ind.code }}'] = '{{ ind.name }}';
        indicatorDescriptionsMap['{{ ind.code }}'] = '{{ ind.description|default:""|escapejs }}';
        indicatorUnitsMap['{{ ind.code }}'] = '{{ ind.unit|default:"-"|escapejs }}';
        indicatorSeriesSemanticsMap['{{ ind.code }}'] = '{{ ind.series_semantics|default:""|escapejs }}';
        indicatorPairedCodesMap['{{ ind.code }}'] = '{{ ind.paired_indicator_code|default:""|escapejs }}';
        indicatorChartPolicyMap['{{ ind.code }}'] = '{{ ind.chart_policy|default:""|escapejs }}';
        indicatorChartResetFrequencyMap['{{ ind.code }}'] = '{{ ind.chart_reset_frequency|default:""|escapejs }}';
        indicatorChartSegmentBasisMap['{{ ind.code }}'] = '{{ ind.chart_segment_basis|default:""|escapejs }}';
    {% endfor %}
{% endfor %}

const initialIndicator = '{{ selected_indicator|escapejs }}';
const initialHistoryRows = JSON.parse(document.getElementById('initialHistoryRowsData').textContent || '[]');
const initialIndicatorMeta = {
    name: '{{ selected_indicator_data.name|default:""|escapejs }}',
    description: '{{ selected_indicator_data.description|default:""|escapejs }}',
    unit: '{{ selected_indicator_data.unit|default:""|escapejs }}',
    periodType: '{{ selected_indicator_data.period_type|default:""|escapejs }}',
    seriesSemantics: '{{ selected_indicator_data.series_semantics|default:""|escapejs }}',
    pairedIndicatorCode: '{{ selected_indicator_data.paired_indicator_code|default:""|escapejs }}',
    chartPolicy: '{{ selected_indicator_data.chart_policy|default:""|escapejs }}',
    chartResetFrequency: '{{ selected_indicator_data.chart_reset_frequency|default:""|escapejs }}',
    chartSegmentBasis: '{{ selected_indicator_data.chart_segment_basis|default:""|escapejs }}',
    latestPeriod: '{{ selected_indicator_data.latest_period|default:""|escapejs }}',
    freshnessStatus: '{{ selected_indicator_data.freshness_status|default:""|escapejs }}',
    decisionGrade: '{{ selected_indicator_data.decision_grade|default:""|escapejs }}',
    blockedReason: '{{ selected_indicator_data.blocked_reason|default:""|escapejs }}',
    latestPublishedAt: '{{ selected_indicator_data.latest_published_at|default:""|escapejs }}',
    latestQuality: '{{ selected_indicator_data.latest_quality|default:""|escapejs }}',
    hasData: {% if selected_indicator_data and selected_indicator_data.has_data %}true{% else %}false{% endif %},
};

let currentIndicatorCode = initialIndicator;
let currentChart = null;
let refreshStatusTimer = null;
let isBulkRefreshing = false;
let currentIndicatorPayload = null;
let currentVisibleRange = { startIndex: 0, endIndex: -1, preset: '36' };
let detailTableScope = 'visible';
let chartResizeHandlerAttached = false;
const autoRefreshAttemptedIndicators = new Set();

document.addEventListener('DOMContentLoaded', function() {
    updateRefreshAllButtonState();
    if (initialIndicator) {
        showIndicatorInfoBar();
        updateRefreshButtonState(initialIndicator);
        const initialPayload = buildInitialPayloadFromServerContext();
        if (initialPayload && initialPayload.data.length > 0) {
            currentIndicatorPayload = initialPayload;
            {
                const chartContext = getCurrentChartContext(initialPayload);
                currentVisibleRange = buildVisibleRange(
                    chartContext ? chartContext.chartConfig.xAxisData.length : initialPayload.data.length,
                    String(DEFAULT_VISIBLE_POINTS),
                );
            }
            detailTableScope = 'visible';
            updateRangeControlsAvailability(true);
            updateDetailButtonState();
            updateIndicatorInfo(initialIndicator, initialPayload);
            renderMainChart(initialIndicator, initialPayload);
            void maybeAutoRefreshIndicator(initialIndicator, initialPayload);
        } else {
            renderNoDataState(initialIndicator);
            void maybeAutoRefreshIndicator(initialIndicator, null);
        }
        return;
    }
    updateRangeControlsAvailability(false);
    updateDetailButtonState();
    showChartPlaceholder('选择一个指标查看趋势', '从左侧列表点击指标名称，默认先展示最近 36 个周期。');
});

document.addEventListener('keydown', function(event) {
    if (event.key === 'Escape') {
        closeDataDetailDrawer();
    }
});

function showIndicatorInfoBar() {
    document.getElementById('indicatorInfoBar').style.display = 'flex';
}

function escapeHtml(value) {
    return String(value ?? '')
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#39;');
}

function escapeMarkdownCell(value) {
    return String(value ?? '-')
        .replace(/\|/g, '\\|')
        .replace(/\n/g, '<br>');
}

function formatMetricValue(value, maximumFractionDigits = 4) {
    const numericValue = Number(value);
    if (!Number.isFinite(numericValue)) {
        return '-';
    }
    return new Intl.NumberFormat('zh-CN', {
        minimumFractionDigits: 0,
        maximumFractionDigits,
    }).format(numericValue);
}

function formatDisplayDate(value) {
    return value ? String(value) : '-';
}

function normalizeChronologicalSeries(data) {
    if (!Array.isArray(data)) {
        return [];
    }
    return [...data].sort((left, right) => {
        const leftPeriod = String(left.reporting_period || '');
        const rightPeriod = String(right.reporting_period || '');
        if (leftPeriod !== rightPeriod) {
            return leftPeriod.localeCompare(rightPeriod);
        }

        const leftRevision = Number(left.revision_number ?? 0);
        const rightRevision = Number(right.revision_number ?? 0);
        if (leftRevision !== rightRevision) {
            return leftRevision - rightRevision;
        }

        const leftPublishedAt = String(left.published_at || '');
        const rightPublishedAt = String(right.published_at || '');
        return leftPublishedAt.localeCompare(rightPublishedAt);
    });
}

function buildSortablePeriodKey(label) {
    const text = String(label || '').trim();
    const quarterMatch = text.match(/^(\d{4})-Q([1-4])$/);
    if (quarterMatch) {
        return `${quarterMatch[1]}-${quarterMatch[2]}`;
    }

    const monthMatch = text.match(/^(\d{4})-(\d{1,2})$/);
    if (monthMatch) {
        return `${monthMatch[1]}-${monthMatch[2].padStart(2, '0')}`;
    }

    const dayMatch = text.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
    if (dayMatch) {
        return `${dayMatch[1]}-${dayMatch[2].padStart(2, '0')}-${dayMatch[3].padStart(2, '0')}`;
    }

    const yearMatch = text.match(/^(\d{4})$/);
    if (yearMatch) {
        return `${yearMatch[1]}-00`;
    }

    return text;
}

function formatReportingPeriodLabel(reportingPeriod, periodType) {
    const rawValue = String(reportingPeriod || '');
    const normalizedPeriodType = String(periodType || '').toUpperCase();
    if (!rawValue) {
        return '-';
    }
    if (normalizedPeriodType === 'Q' && rawValue.length >= 7) {
        const quarter = getQuarterKeyFromReportingPeriod(rawValue);
        if (quarter) {
            return `${rawValue.slice(0, 4)}-${quarter}`;
        }
    }
    if (normalizedPeriodType === 'M' && rawValue.length >= 7) {
        return rawValue.slice(0, 7);
    }
    if (normalizedPeriodType === 'Y' && rawValue.length >= 4) {
        return rawValue.slice(0, 4);
    }
    return rawValue;
}

function normalizeMacroPayload(code, result) {
    const normalizedData = normalizeChronologicalSeries(Array.isArray(result?.data) ? result.data : []);
    return {
        indicator_code: code,
        name_cn: result?.name_cn || indicatorNamesMap[code] || code,
        description: result?.description || indicatorDescriptionsMap[code] || '',
        period_type: result?.period_type || '',
        series_semantics: result?.series_semantics || indicatorSeriesSemanticsMap[code] || '',
        paired_indicator_code: result?.paired_indicator_code || indicatorPairedCodesMap[code] || '',
        chart_policy: result?.chart_policy || indicatorChartPolicyMap[code] || '',
        chart_reset_frequency: result?.chart_reset_frequency || indicatorChartResetFrequencyMap[code] || '',
        chart_segment_basis: result?.chart_segment_basis || indicatorChartSegmentBasisMap[code] || '',
        total: Number(result?.total ?? normalizedData.length),
        data: normalizedData,
        data_source: result?.data_source || '',
        freshness_status: result?.freshness_status || '',
        decision_grade: result?.decision_grade || '',
        must_not_use_for_decision: Boolean(result?.must_not_use_for_decision),
        blocked_reason: result?.blocked_reason || '',
        latest_reporting_period: result?.latest_reporting_period || '',
        latest_published_at: result?.latest_published_at || '',
        latest_quality: result?.latest_quality || '',
        provenance_class: result?.provenance_class || '',
        provenance_label: result?.provenance_label || '',
        publisher: result?.publisher || '',
        publisher_code: result?.publisher_code || '',
        publisher_codes: Array.isArray(result?.publisher_codes) ? result.publisher_codes : [],
        access_channel: result?.access_channel || '',
        derivation_method: result?.derivation_method || '',
        upstream_indicator_codes: Array.isArray(result?.upstream_indicator_codes) ? result.upstream_indicator_codes : [],
        is_derived: Boolean(result?.is_derived),
    };
}

function buildInitialPayloadFromServerContext() {
    if (!initialIndicatorMeta.hasData || !initialIndicator || !Array.isArray(initialHistoryRows) || initialHistoryRows.length === 0) {
        return null;
    }
    return normalizeMacroPayload(initialIndicator, {
        indicator_code: initialIndicator,
        name_cn: initialIndicatorMeta.name || indicatorNamesMap[initialIndicator] || initialIndicator,
        description: initialIndicatorMeta.description || indicatorDescriptionsMap[initialIndicator] || '',
        period_type: initialIndicatorMeta.periodType || '',
        series_semantics: initialIndicatorMeta.seriesSemantics || indicatorSeriesSemanticsMap[initialIndicator] || '',
        paired_indicator_code: initialIndicatorMeta.pairedIndicatorCode || indicatorPairedCodesMap[initialIndicator] || '',
        chart_policy: initialIndicatorMeta.chartPolicy || indicatorChartPolicyMap[initialIndicator] || '',
        chart_reset_frequency: initialIndicatorMeta.chartResetFrequency || indicatorChartResetFrequencyMap[initialIndicator] || '',
        chart_segment_basis: initialIndicatorMeta.chartSegmentBasis || indicatorChartSegmentBasisMap[initialIndicator] || '',
        latest_reporting_period: initialIndicatorMeta.latestPeriod || '',
        freshness_status: initialIndicatorMeta.freshnessStatus || '',
        decision_grade: initialIndicatorMeta.decisionGrade || '',
        blocked_reason: initialIndicatorMeta.blockedReason || '',
        latest_published_at: initialIndicatorMeta.latestPublishedAt || '',
        latest_quality: initialIndicatorMeta.latestQuality || '',
        data_source: 'server_snapshot',
        data: initialHistoryRows.map(row => ({
            reporting_period: row.reporting_period,
            value: row.storage_value ?? row.value,
            unit: row.storage_unit || row.unit || initialIndicatorMeta.unit || '',
            display_value: row.display_value ?? row.value,
            display_unit: row.display_unit || row.unit || initialIndicatorMeta.unit || '',
            original_unit: row.original_unit || row.unit || initialIndicatorMeta.unit || '',
            source: row.source || 'data_center_fact',
            quality: row.quality || '',
            published_at: row.published_at || null,
            age_days: row.age_days ?? '',
            freshness_status: row.freshness_status || '',
            decision_grade: row.decision_grade || '',
            revision_number: row.revision_number ?? 0,
        })),
    });
}

function getPayloadGovernanceConfig(payload) {
    const chartPolicy = String(payload?.chart_policy || '');
    const pairedIndicatorCode = String(payload?.paired_indicator_code || '');
    const chartResetFrequency = String(payload?.chart_reset_frequency || '');
    const chartSegmentBasis = String(payload?.chart_segment_basis || '');
    if (usesResetStackChart({ chartPolicy, chartSegmentBasis })) {
        const resetLabelMap = {
            year: '每年',
            quarter: '每季度',
            month: '每月',
        };
        const monthlyCumulativeNote = String(payload?.period_type || '').toUpperCase() === 'M'
            ? ' 对月度年内累计序列，如果官方口径不单独发布 1 月值，年度首个分段会直接表示“1-首个发布月累计”。'
            : '';
        const resetLabel = resetLabelMap[chartResetFrequency] || '每个重置周期';
        return {
            chartPolicy,
            chartResetFrequency,
            chartSegmentBasis,
            note: pairedIndicatorCode
                ? `该指标是周期内累计值口径，跨重置周期会自然回到起点，不能直接连成连续趋势线；图表已切换为“${resetLabel}一个柱子、柱内按周期位次分段”的堆叠柱状图。${monthlyCumulativeNote} 判断增长方向请结合配对指标 ${pairedIndicatorCode}。`
                : `该指标是周期内累计值口径，跨重置周期会自然回到起点，不能直接连成连续趋势线；图表已切换为“${resetLabel}一个柱子、柱内按周期位次分段”的堆叠柱状图。${monthlyCumulativeNote}`,
        };
    }
    return {
        chartPolicy,
        chartResetFrequency,
        chartSegmentBasis,
        note: '',
    };
}

function usesResetStackChart(governance) {
    const chartPolicy = String(governance?.chartPolicy || governance?.chart_policy || '');
    const chartSegmentBasis = String(
        governance?.chartSegmentBasis || governance?.chart_segment_basis || ''
    ).toLowerCase();
    return RESET_STACK_CHART_POLICIES.has(chartPolicy) || chartSegmentBasis === 'period_delta';
}

function usesYearlyMonthlyCumulativeReset(governance, periodType) {
    return (
        usesResetStackChart(governance)
        && String(governance?.chartResetFrequency || governance?.chart_reset_frequency || '').toLowerCase() === 'year'
        && String(periodType || '').toUpperCase() === 'M'
    );
}

function buildChartPoints(payload) {
    if (!payload || !Array.isArray(payload.data)) {
        return [];
    }
    return payload.data.map(row => ({
        label: formatReportingPeriodLabel(row.reporting_period, payload.period_type),
        value: Number(row.display_value ?? row.value),
        displayUnit: row.display_unit || row.original_unit || row.unit || '',
        periodType: String(row.period_type || payload.period_type || ''),
        row,
    }));
}

function getQuarterKeyFromReportingPeriod(reportingPeriod) {
    const normalizedReportingPeriod = String(reportingPeriod || '');
    if (normalizedReportingPeriod.length < 7) {
        return 'Q?';
    }
    const monthNumber = Number.parseInt(normalizedReportingPeriod.slice(5, 7), 10);
    if (!Number.isFinite(monthNumber) || monthNumber < 1 || monthNumber > 12) {
        return 'Q?';
    }
    return `Q${Math.floor((monthNumber - 1) / 3) + 1}`;
}

function getPointCycleKey(point, periodType) {
    const reportingPeriod = String(point?.row?.reporting_period || '');
    const normalizedPeriodType = String(periodType || '').toUpperCase();
    if (normalizedPeriodType === 'Q' && reportingPeriod.length >= 7) {
        return getQuarterKeyFromReportingPeriod(reportingPeriod);
    }
    if (normalizedPeriodType === 'M' && reportingPeriod.length >= 7) {
        return reportingPeriod.slice(5, 7);
    }
    return String(point?.label || '');
}

function getResetGroupKey(point, resetFrequency) {
    const reportingPeriod = String(point?.row?.reporting_period || '');
    const normalizedResetFrequency = String(resetFrequency || '').toLowerCase();
    if (normalizedResetFrequency === 'quarter' && reportingPeriod.length >= 7) {
        return `${reportingPeriod.slice(0, 4)}-${getQuarterKeyFromReportingPeriod(reportingPeriod)}`;
    }
    if (normalizedResetFrequency === 'month' && reportingPeriod.length >= 7) {
        return reportingPeriod.slice(0, 7);
    }
    if (reportingPeriod.length >= 4) {
        return reportingPeriod.slice(0, 4);
    }
    return String(point?.label || '');
}

function getCycleSequence(periodType, chartPoints) {
    const normalizedPeriodType = String(periodType || '').toUpperCase();
    if (normalizedPeriodType === 'Q') {
        return ['Q1', 'Q2', 'Q3', 'Q4'];
    }
    if (normalizedPeriodType === 'M') {
        return ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
    }
    const discovered = [];
    chartPoints.forEach(point => {
        const cycleKey = getPointCycleKey(point, normalizedPeriodType);
        if (cycleKey && !discovered.includes(cycleKey)) {
            discovered.push(cycleKey);
        }
    });
    return discovered;
}

function getMinimumObservedCycleMonth(chartPoints) {
    const monthNumbers = (Array.isArray(chartPoints) ? chartPoints : [])
        .map(point => Number.parseInt(getPointCycleKey(point, 'M'), 10))
        .filter(monthNumber => Number.isFinite(monthNumber) && monthNumber >= 1 && monthNumber <= 12);
    if (monthNumbers.length === 0) {
        return null;
    }
    return Math.min(...monthNumbers);
}

function formatCycleDisplayName(cycleKey, periodType, governance = null, chartPoints = []) {
    const normalizedPeriodType = String(periodType || '').toUpperCase();
    if (normalizedPeriodType === 'M') {
        const monthNumber = Number.parseInt(String(cycleKey), 10);
        if (!Number.isFinite(monthNumber)) {
            return String(cycleKey || '');
        }
        if (usesYearlyMonthlyCumulativeReset(governance, normalizedPeriodType)) {
            const minimumObservedMonth = getMinimumObservedCycleMonth(chartPoints);
            if (minimumObservedMonth && minimumObservedMonth > 1 && monthNumber === minimumObservedMonth) {
                return `1-${minimumObservedMonth}月累计`;
            }
        }
        return `${monthNumber}月`;
    }
    return String(cycleKey || '');
}

function getCycleColor(cycleKey, cycleIndex, periodType) {
    const normalizedPeriodType = String(periodType || '').toUpperCase();
    if (normalizedPeriodType === 'Q') {
        const quarterPalette = {
            Q1: '#2563EB',
            Q2: '#0F766E',
            Q3: '#CA8A04',
            Q4: '#C2410C',
            'Q?': '#64748B',
        };
        return quarterPalette[cycleKey] || '#64748B';
    }

    if (normalizedPeriodType === 'M') {
        const monthPalette = [
            '#1D4ED8', '#2563EB', '#3B82F6', '#60A5FA',
            '#0F766E', '#14B8A6', '#2DD4BF', '#5EEAD4',
            '#CA8A04', '#EAB308', '#F59E0B', '#F97316',
        ];
        return monthPalette[cycleIndex % monthPalette.length];
    }

    const fallbackPalette = ['#2563EB', '#0F766E', '#CA8A04', '#C2410C', '#7C3AED', '#64748B'];
    return fallbackPalette[cycleIndex % fallbackPalette.length];
}

function buildResetStackChartConfig(chartPoints, governance) {
    const periodType = String(chartPoints?.[0]?.periodType || '').toUpperCase();
    const resetFrequency = String(governance?.chartResetFrequency || 'year').toLowerCase();
    const segmentBasis = String(governance?.chartSegmentBasis || 'period_delta').toLowerCase();
    const cycleSequence = getCycleSequence(periodType, chartPoints);
    const groupedByResetCycle = new Map();

    chartPoints.forEach(point => {
        const groupKey = getResetGroupKey(point, resetFrequency);
        if (!groupedByResetCycle.has(groupKey)) {
            groupedByResetCycle.set(groupKey, []);
        }
        groupedByResetCycle.get(groupKey).push(point);
    });

    const xAxisData = Array.from(groupedByResetCycle.keys()).sort();
    const seriesDataMap = Object.fromEntries(
        cycleSequence.map(cycleKey => [cycleKey, new Array(xAxisData.length).fill(null)])
    );
    const tooltipMap = {};

    xAxisData.forEach((groupKey, groupIndex) => {
        const points = (groupedByResetCycle.get(groupKey) || []).slice().sort((left, right) => (
            buildSortablePeriodKey(left.label).localeCompare(buildSortablePeriodKey(right.label))
        ));
        let previousCumulative = 0;
        let latestPoint = null;
        const groupTooltip = {};

        points.forEach(point => {
            const cycleKey = getPointCycleKey(point, periodType);
            const cumulativeValue = Number(point.value);
            if (!Number.isFinite(cumulativeValue)) {
                return;
            }
            const segmentValue = segmentBasis === 'period_delta'
                ? (cumulativeValue >= previousCumulative
                    ? cumulativeValue - previousCumulative
                    : cumulativeValue)
                : cumulativeValue;
            previousCumulative = cumulativeValue;
            latestPoint = point;

            if (!(cycleKey in seriesDataMap)) {
                return;
            }
            seriesDataMap[cycleKey][groupIndex] = segmentValue;
            groupTooltip[cycleKey] = {
                point,
                segmentValue,
                cumulativeValue,
            };
        });

        tooltipMap[groupKey] = {
            cycles: groupTooltip,
            latestPoint,
        };
    });

    return {
        xAxisData,
        legendData: cycleSequence.map(cycleKey => formatCycleDisplayName(cycleKey, periodType, governance, chartPoints)),
        xAxisLabelRotate: 0,
        tooltipFormatter: function(params) {
            const rows = Array.isArray(params) ? params : [];
            const groupKey = String(rows?.[0]?.axisValue || '');
            const tooltipEntry = tooltipMap[groupKey] || { cycles: {}, latestPoint: null };
            const latestPoint = tooltipEntry.latestPoint;
            const tooltipLines = [groupKey || '-'];

            cycleSequence.forEach((cycleKey, cycleIndex) => {
                const cycleEntry = tooltipEntry.cycles[cycleKey];
                if (!cycleEntry) {
                    return;
                }
                const color = getCycleColor(cycleKey, cycleIndex, periodType);
                const cycleName = formatCycleDisplayName(cycleKey, periodType, governance, chartPoints);
                const suffix = cycleEntry.point?.displayUnit && cycleEntry.point.displayUnit !== '-'
                    ? ` ${cycleEntry.point.displayUnit}`
                    : '';
                tooltipLines.push(
                    `<span style="display:inline-block;margin-right:6px;border-radius:50%;width:8px;height:8px;background:${color};"></span>${cycleName}: 当期 ${formatMetricValue(cycleEntry.segmentValue)}${suffix} / 累计 ${formatMetricValue(cycleEntry.cumulativeValue)}${suffix}`
                );
            });

            tooltipLines.push(`来源: ${latestPoint?.row?.source || '-'}`);
            tooltipLines.push(`发布时间: ${latestPoint?.row?.published_at || '-'}`);
            return tooltipLines.join('<br/>');
        },
        series: cycleSequence.map((cycleKey, cycleIndex) => ({
            name: formatCycleDisplayName(cycleKey, periodType, governance, chartPoints),
            type: 'bar',
            stack: 'cycle-stack',
            data: seriesDataMap[cycleKey],
            barMaxWidth: 52,
            itemStyle: {
                color: getCycleColor(cycleKey, cycleIndex, periodType),
            },
            emphasis: {
                itemStyle: {
                    opacity: 0.92,
                },
            },
        })),
    };
}

function buildChartConfig(chartPoints, governance) {
    const baseStyle = {
        type: 'line',
        smooth: true,
        connectNulls: false,
        showSymbol: true,
        symbolSize: 6,
        itemStyle: { color: '#2563EB' },
        lineStyle: { width: 2.5, color: '#2563EB' },
    };

    if (governance.chartPolicy === 'period_bar') {
        return {
            xAxisData: chartPoints.map(item => item.label),
            legendShow: false,
            xAxisLabelRotate: 45,
            tooltipFormatter: null,
            series: [
                {
                    type: 'bar',
                    data: chartPoints.map(item => item.value),
                    barMaxWidth: 34,
                    itemStyle: {
                        borderRadius: [6, 6, 0, 0],
                        color: '#2563EB',
                    },
                    emphasis: {
                        itemStyle: {
                            opacity: 0.9,
                        },
                    },
                }
            ],
        };
    }

    if (!usesResetStackChart(governance)) {
        return {
            xAxisData: chartPoints.map(item => item.label),
            legendShow: false,
            xAxisLabelRotate: 45,
            tooltipFormatter: null,
            series: [
                {
                    ...baseStyle,
                    data: chartPoints.map(item => item.value),
                    areaStyle: {
                        color: {
                            type: 'linear',
                            x: 0, y: 0, x2: 0, y2: 1,
                            colorStops: [
                                { offset: 0, color: 'rgba(37, 99, 235, 0.3)' },
                                { offset: 1, color: 'rgba(37, 99, 235, 0.05)' }
                            ]
                        }
                    }
                }
            ],
        };
    }

    return {
        ...buildResetStackChartConfig(chartPoints, governance),
        legendShow: false,
    };
}

function buildVisibleRange(totalPoints, preset) {
    if (totalPoints <= 0) {
        return { startIndex: 0, endIndex: -1, preset: preset === 'all' ? 'all' : '36' };
    }
    if (preset === 'all') {
        return { startIndex: 0, endIndex: totalPoints - 1, preset: 'all' };
    }
    const desiredCount = Number.parseInt(String(preset), 10);
    const safeCount = Number.isFinite(desiredCount) && desiredCount > 0
        ? Math.min(desiredCount, totalPoints)
        : Math.min(DEFAULT_VISIBLE_POINTS, totalPoints);
    return {
        startIndex: Math.max(totalPoints - safeCount, 0),
        endIndex: totalPoints - 1,
        preset: String(safeCount),
    };
}

function normalizeVisibleRange(range, totalPoints) {
    if (totalPoints <= 0) {
        return { startIndex: 0, endIndex: -1, preset: '36' };
    }
    const startIndex = Math.max(0, Math.min(totalPoints - 1, Number(range?.startIndex ?? 0)));
    const endIndex = Math.max(startIndex, Math.min(totalPoints - 1, Number(range?.endIndex ?? totalPoints - 1)));
    return {
        startIndex,
        endIndex,
        preset: range?.preset ?? null,
    };
}

function getCurrentChartContext(payload = currentIndicatorPayload) {
    if (!payload || !Array.isArray(payload.data) || payload.data.length === 0) {
        return null;
    }
    const chartPoints = buildChartPoints(payload);
    const governance = getPayloadGovernanceConfig(payload);
    const chartConfig = buildChartConfig(chartPoints, governance);
    return { chartPoints, governance, chartConfig };
}

function getVisiblePointCount() {
    const chartContext = getCurrentChartContext();
    if (!chartContext) {
        return 0;
    }
    const normalizedRange = normalizeVisibleRange(
        currentVisibleRange,
        chartContext.chartConfig.xAxisData.length,
    );
    if (normalizedRange.endIndex < normalizedRange.startIndex) {
        return 0;
    }
    return normalizedRange.endIndex - normalizedRange.startIndex + 1;
}

function getVisibleRows() {
    if (!currentIndicatorPayload || !Array.isArray(currentIndicatorPayload.data)) {
        return [];
    }
    const chartContext = getCurrentChartContext();
    if (!chartContext) {
        return [];
    }
    const normalizedRange = normalizeVisibleRange(
        currentVisibleRange,
        chartContext.chartConfig.xAxisData.length,
    );
    if (normalizedRange.endIndex < normalizedRange.startIndex) {
        return [];
    }
    if (!usesResetStackChart(chartContext.governance)) {
        return currentIndicatorPayload.data.slice(
            normalizedRange.startIndex,
            normalizedRange.endIndex + 1,
        );
    }
    const visibleGroupKeys = new Set(
        chartContext.chartConfig.xAxisData.slice(
            normalizedRange.startIndex,
            normalizedRange.endIndex + 1,
        ),
    );
    return currentIndicatorPayload.data.filter(row => {
        const point = {
            label: formatReportingPeriodLabel(row.reporting_period, currentIndicatorPayload.period_type),
            row,
        };
        return visibleGroupKeys.has(
            getResetGroupKey(point, chartContext.governance.chartResetFrequency),
        );
    });
}

function buildVisibleRangeLabel() {
    const chartContext = getCurrentChartContext();
    if (!chartContext) {
        return '-';
    }
    const normalizedRange = normalizeVisibleRange(
        currentVisibleRange,
        chartContext.chartConfig.xAxisData.length,
    );
    if (normalizedRange.endIndex < normalizedRange.startIndex) {
        return '-';
    }
    if (usesResetStackChart(chartContext.governance)) {
        const visibleAxis = chartContext.chartConfig.xAxisData.slice(
            normalizedRange.startIndex,
            normalizedRange.endIndex + 1,
        );
        if (visibleAxis.length === 0) {
            return '-';
        }
        return `${visibleAxis[0]} ~ ${visibleAxis[visibleAxis.length - 1]}`;
    }
    const visibleRows = getVisibleRows();
    if (visibleRows.length === 0) {
        return '-';
    }
    return `${formatReportingPeriodLabel(visibleRows[0].reporting_period, currentIndicatorPayload?.period_type)} ~ ${formatReportingPeriodLabel(visibleRows[visibleRows.length - 1].reporting_period, currentIndicatorPayload?.period_type)}`;
}

function updateRangePresetButtons() {
    const visiblePointCount = getVisiblePointCount();
    document.querySelectorAll('.range-preset-btn').forEach(button => {
        const preset = button.dataset.range;
        const active = preset === 'all'
            ? currentVisibleRange.preset === 'all'
            : currentVisibleRange.preset === preset && visiblePointCount === Number(preset);
        button.classList.toggle('active', active);
    });
}

function updateRangeControlsAvailability(enabled) {
    document.querySelectorAll('.range-preset-btn').forEach(button => {
        button.disabled = !enabled;
    });
    updateRangePresetButtons();
}

function updateDetailButtonState() {
    const detailButton = document.getElementById('openDataDetailBtn');
    if (!detailButton) {
        return;
    }
    detailButton.disabled = !currentIndicatorPayload || !Array.isArray(currentIndicatorPayload.data) || currentIndicatorPayload.data.length === 0;
}

function updateVisibleRangeSummary() {
    const summaryEl = document.getElementById('currentRangeSummary');
    const recordEl = document.getElementById('currentIndicatorRecords');
    if (!summaryEl || !recordEl) {
        return;
    }
    if (!currentIndicatorPayload || !Array.isArray(currentIndicatorPayload.data) || currentIndicatorPayload.data.length === 0) {
        summaryEl.textContent = '暂无可展示区间';
        recordEl.textContent = '0 条';
        return;
    }
    const chartContext = getCurrentChartContext();
    const visibleRows = getVisibleRows();
    if (chartContext && usesResetStackChart(chartContext.governance)) {
        const totalPoints = chartContext.chartConfig.xAxisData.length;
        const visiblePointCount = getVisiblePointCount();
        summaryEl.textContent = `当前显示 ${visiblePointCount}/${totalPoints} 个周期 · ${buildVisibleRangeLabel()}`;
        recordEl.textContent = `${totalPoints} 个周期 / 覆盖 ${visibleRows.length} 条原始记录`;
        return;
    }
    summaryEl.textContent = `当前显示 ${visibleRows.length}/${currentIndicatorPayload.data.length} 条 · ${buildVisibleRangeLabel()}`;
    recordEl.textContent = `${currentIndicatorPayload.data.length} 条 / 显示 ${visibleRows.length} 条`;
}

function syncVisibleRangeDependentUI() {
    updateRangePresetButtons();
    updateVisibleRangeSummary();
    renderDataDetailPanel();
}

function applyRangePreset(preset) {
    const chartContext = getCurrentChartContext();
    if (!chartContext) {
        return;
    }
    currentVisibleRange = buildVisibleRange(chartContext.chartConfig.xAxisData.length, preset);
    renderMainChart(currentIndicatorCode, currentIndicatorPayload);
}

function clearChart() {
    if (currentChart) {
        currentChart.dispose();
        currentChart = null;
    }
}

function showChartPlaceholder(title, description) {
    clearChart();
    document.getElementById('chartPlaceholderTitle').textContent = title;
    document.getElementById('chartPlaceholderDesc').textContent = description;
    document.getElementById('chartPlaceholderAction').innerHTML = '';
    document.getElementById('chartPlaceholder').style.display = 'block';
    updateRangeControlsAvailability(false);
    updateDetailButtonState();
}

function setRefreshStatus(message, tone = 'muted') {
    const statusEl = document.getElementById('refreshIndicatorStatus');
    if (!statusEl) {
        return;
    }
    statusEl.textContent = message || '';
    statusEl.className = `refresh-status ${tone}`;
    if (refreshStatusTimer) {
        window.clearTimeout(refreshStatusTimer);
        refreshStatusTimer = null;
    }
    if (message && (tone === 'success' || tone === 'warning')) {
        refreshStatusTimer = window.setTimeout(() => {
            statusEl.textContent = '';
            statusEl.className = 'refresh-status muted';
            refreshStatusTimer = null;
        }, 5000);
    }
}

function getIndicatorItem(code) {
    return document.querySelector(`.ind-item[data-code="${code}"]`);
}

function getManualRefreshStart(code) {
    const item = getIndicatorItem(code);
    return item ? item.dataset.refreshStart : '2010-01-01';
}

function isIndicatorSyncSupported(code) {
    const item = getIndicatorItem(code);
    return item ? item.dataset.syncSupported === 'true' : false;
}

function getPayloadLatestPoint(payload) {
    if (!payload || !Array.isArray(payload.data) || payload.data.length === 0) {
        return null;
    }
    return payload.data[payload.data.length - 1];
}

function shouldAutoRefreshIndicator(code, payload) {
    if (!code || autoRefreshAttemptedIndicators.has(code)) {
        return false;
    }
    if (isBulkRefreshing || !manualRefreshProviderId || !isIndicatorSyncSupported(code)) {
        return false;
    }
    if (!payload || !Array.isArray(payload.data) || payload.data.length === 0) {
        return true;
    }
    const latest = getPayloadLatestPoint(payload);
    const freshnessStatus = String(payload.freshness_status || latest?.freshness_status || '').toLowerCase();
    const decisionGrade = String(payload.decision_grade || latest?.decision_grade || '').toLowerCase();
    return freshnessStatus === 'stale' || decisionGrade === 'degraded';
}

async function maybeAutoRefreshIndicator(code, payload) {
    if (!shouldAutoRefreshIndicator(code, payload)) {
        return false;
    }

    autoRefreshAttemptedIndicators.add(code);
    const hasData = Boolean(payload && Array.isArray(payload.data) && payload.data.length > 0);
    const statusMessage = hasData
        ? '检测到当前指标数据已过期，正在自动刷新...'
        : '检测到当前指标尚未入库，正在自动抓取...';

    updateRefreshButtonState(code, true);
    setRefreshStatus(statusMessage, 'warning');

    try {
        const refreshPayload = await requestIndicatorRefresh(code);
        markIndicatorAsSynced(code);
        const rowCount = await selectIndicator(code, {
            keepRefreshStatus: true,
            skipAutoRefresh: true,
        });
        if (rowCount > 0) {
            setRefreshStatus(`自动刷新完成，已更新 ${rowCount} 条展示数据`, 'success');
        } else if (Number(refreshPayload.stored_count || 0) > 0) {
            setRefreshStatus(`自动刷新完成，已写入 ${refreshPayload.stored_count} 条记录`, 'warning');
        } else {
            setRefreshStatus('自动刷新完成，但当前数据源未返回可入库记录', 'warning');
        }
        return true;
    } catch (error) {
        setRefreshStatus(`自动刷新失败：${error.message || '请稍后重试'}`, 'error');
        return false;
    } finally {
        updateRefreshButtonState(code, false);
    }
}

function updateRefreshButtonState(code, isLoading = false) {
    const button = document.getElementById('refreshIndicatorBtn');
    if (!button) {
        return;
    }
    const hasProvider = manualRefreshProviderId > 0;
    const hasIndicator = Boolean(code);
    const syncSupported = hasIndicator && isIndicatorSyncSupported(code);
    button.disabled = !hasProvider || !hasIndicator || !syncSupported || isLoading || isBulkRefreshing;
    button.textContent = isLoading ? '抓取中...' : '手动刷新抓取';
    if (isBulkRefreshing) {
        button.title = '全部抓取进行中，暂不支持单指标刷新';
        return;
    }
    if (!hasProvider) {
        button.title = '当前未配置可用的 Data Center 宏观 provider';
        return;
    }
    if (!hasIndicator) {
        button.title = '请先选择一个指标';
        return;
    }
    if (!syncSupported) {
        button.title = '该指标尚未接入自动同步链路';
        return;
    }
    button.title = `抓取区间：${getManualRefreshStart(code)} ~ ${manualRefreshEndDate || '今天'}`;
}

function updateRefreshAllButtonState(isLoading = false) {
    const button = document.getElementById('refreshAllIndicatorsBtn');
    if (!button) {
        return;
    }
    const hasProvider = manualRefreshProviderId > 0;
    const hasSupportedIndicators = bulkRefreshIndicatorCodes.length > 0;
    const loading = isLoading || isBulkRefreshing;
    button.disabled = !hasProvider || !hasSupportedIndicators || loading;
    button.textContent = loading ? '全部抓取中...' : '全部刷新可抓取指标';
    if (!hasProvider) {
        button.title = '当前未配置可用的 Data Center 宏观 provider';
        return;
    }
    if (!hasSupportedIndicators) {
        button.title = '当前目录中没有已接入自动同步的指标';
        return;
    }
    button.title = `按目录中 ${bulkRefreshIndicatorCodes.length} 个已接入指标串行补抓`;
}

function markIndicatorAsSynced(code) {
    const item = getIndicatorItem(code);
    if (!item) {
        return;
    }
    item.dataset.hasData = 'true';
    const badge = item.querySelector('.ind-status-badge');
    if (badge) {
        badge.remove();
    }
}

async function requestIndicatorRefresh(code) {
    if (!manualRefreshProviderId) {
        throw new Error('当前未配置可用的 Data Center 宏观 provider');
    }
    if (!isIndicatorSyncSupported(code)) {
        throw new Error(`指标 ${code} 尚未接入自动同步链路`);
    }

    const response = await fetch(macroSyncUrl, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRFToken': getCookie('csrftoken')
        },
        body: JSON.stringify({
            provider_id: manualRefreshProviderId,
            indicator_code: code,
            start: getManualRefreshStart(code),
            end: manualRefreshEndDate
        })
    });
    const payload = await response.json().catch(() => ({}));

    if (!response.ok || payload.status !== 'success') {
        throw new Error(payload.detail || payload.error_message || `抓取 ${code} 失败`);
    }
    return payload;
}

async function selectIndicator(code, options = {}) {
    const { keepRefreshStatus = false, skipAutoRefresh = false } = options;
    currentIndicatorCode = code;
    updateRefreshButtonState(code);
    if (!keepRefreshStatus) {
        setRefreshStatus('', 'muted');
    }

    document.querySelectorAll('.ind-item').forEach(el => {
        el.classList.remove('active');
        if (el.dataset.code === code) {
            el.classList.add('active');
        }
    });

    collapseAllCategories();
    const currentItem = document.querySelector(`.ind-item[data-code="${code}"]`);
    if (currentItem) {
        const catContent = currentItem.closest('.cat-content');
        if (catContent) {
            catContent.style.display = 'block';
            const catId = catContent.id.replace('cat-content-', '');
            const icon = document.getElementById(`cat-icon-${catId}`);
            if (icon) {
                icon.textContent = '▼';
            }
        }
    }

    showIndicatorInfoBar();

    try {
        const response = await fetch(`${macroSeriesUrl}?indicator_code=${encodeURIComponent(code)}&limit=500`);
        const result = await response.json().catch(() => ({}));

        if (response.ok && Array.isArray(result.data)) {
            if (result.data.length === 0) {
                renderNoDataState(code);
                if (!skipAutoRefresh) {
                    await maybeAutoRefreshIndicator(code, null);
                }
                return 0;
            }
            currentIndicatorPayload = normalizeMacroPayload(code, result);
            {
                const chartContext = getCurrentChartContext(currentIndicatorPayload);
                currentVisibleRange = buildVisibleRange(
                    chartContext ? chartContext.chartConfig.xAxisData.length : currentIndicatorPayload.data.length,
                    String(DEFAULT_VISIBLE_POINTS),
                );
            }
            detailTableScope = 'visible';
            updateRangeControlsAvailability(true);
            updateDetailButtonState();
            updateIndicatorInfo(code, currentIndicatorPayload);
            renderMainChart(code, currentIndicatorPayload);
            if (!skipAutoRefresh) {
                await maybeAutoRefreshIndicator(code, currentIndicatorPayload);
            }
            return currentIndicatorPayload.data.length;
        }
    } catch (error) {
        // 空态由下方统一处理
    }

    renderNoDataState(code);
    if (!skipAutoRefresh) {
        await maybeAutoRefreshIndicator(code, null);
    }
    return 0;
}

async function refreshIndicator(code = currentIndicatorCode) {
    if (!code) {
        return;
    }

    if (!manualRefreshProviderId) {
        setRefreshStatus('当前未配置可用的 Data Center 宏观 provider', 'error');
        updateRefreshButtonState(code);
        return;
    }

    updateRefreshButtonState(code, true);
    setRefreshStatus('正在抓取当前指标...', 'muted');

    try {
        const payload = await requestIndicatorRefresh(code);
        markIndicatorAsSynced(code);
        const rowCount = await selectIndicator(code, { keepRefreshStatus: true });
        if (rowCount > 0) {
            setRefreshStatus(`抓取完成，已更新 ${rowCount} 条展示数据`, 'success');
        } else if (Number(payload.stored_count || 0) > 0) {
            setRefreshStatus(`抓取完成，已写入 ${payload.stored_count} 条记录`, 'warning');
        } else {
            setRefreshStatus('抓取完成，但当前数据源未返回可入库记录', 'warning');
        }
    } catch (error) {
        setRefreshStatus(error.message || '抓取失败，请稍后重试', 'error');
    } finally {
        updateRefreshButtonState(code, false);
    }
}

async function refreshAllIndicators() {
    if (isBulkRefreshing) {
        return;
    }
    if (!manualRefreshProviderId) {
        setRefreshStatus('当前未配置可用的 Data Center 宏观 provider', 'error');
        updateRefreshAllButtonState();
        return;
    }
    if (bulkRefreshIndicatorCodes.length === 0) {
        setRefreshStatus('当前目录中没有已接入自动同步的指标', 'warning');
        updateRefreshAllButtonState();
        return;
    }

    isBulkRefreshing = true;
    updateRefreshAllButtonState(true);
    updateRefreshButtonState(currentIndicatorCode);

    let successCount = 0;
    const failures = [];

    try {
        for (let index = 0; index < bulkRefreshIndicatorCodes.length; index += 1) {
            const code = bulkRefreshIndicatorCodes[index];
            setRefreshStatus(
                `正在批量抓取 (${index + 1}/${bulkRefreshIndicatorCodes.length})：${code}`,
                'muted',
            );
            try {
                await requestIndicatorRefresh(code);
                markIndicatorAsSynced(code);
                successCount += 1;
            } catch (error) {
                failures.push(error.message || `${code} 抓取失败`);
            }
        }

        if (currentIndicatorCode) {
            await selectIndicator(currentIndicatorCode, { keepRefreshStatus: true });
        }

        if (failures.length === 0) {
            setRefreshStatus(`批量抓取完成，成功刷新 ${successCount} 个指标`, 'success');
        } else if (successCount > 0) {
            setRefreshStatus(
                `批量抓取完成：成功 ${successCount} 个，失败 ${failures.length} 个；首个失败：${failures[0]}`,
                'warning',
            );
        } else {
            setRefreshStatus(`批量抓取失败：${failures[0]}`, 'error');
        }
    } finally {
        isBulkRefreshing = false;
        updateRefreshAllButtonState(false);
        updateRefreshButtonState(currentIndicatorCode);
    }
}

function renderMainChart(code, payload) {
    const chartDom = document.getElementById('mainChart');
    const chartPoints = buildChartPoints(payload);
    const governance = getPayloadGovernanceConfig(payload);
    if (chartPoints.length === 0) {
        renderNoDataState(code);
        return;
    }
    const chartConfig = buildChartConfig(chartPoints, governance);

    currentVisibleRange = normalizeVisibleRange(currentVisibleRange, chartConfig.xAxisData.length);
    const totalPoints = chartConfig.xAxisData.length;
    const startPercent = totalPoints <= 1 ? 0 : (currentVisibleRange.startIndex / (totalPoints - 1)) * 100;
    const endPercent = totalPoints <= 1 ? 100 : (currentVisibleRange.endIndex / (totalPoints - 1)) * 100;

    clearChart();
    document.getElementById('chartPlaceholder').style.display = 'none';

    currentChart = echarts.init(chartDom);
    currentChart.setOption({
        title: {
            text: payload.name_cn || indicatorNamesMap[code] || code,
            left: 'center',
            textStyle: { fontSize: 14 }
        },
        tooltip: {
            trigger: 'axis',
            formatter: chartConfig.tooltipFormatter || function(params) {
                const activeParam = Array.isArray(params)
                    ? params.find(item => item && Number.isFinite(chartPoints[item.dataIndex]?.value))
                    : null;
                const dataIndex = activeParam?.dataIndex ?? params?.[0]?.dataIndex ?? 0;
                const point = chartPoints[dataIndex];
                const row = point ? point.row : null;
                const suffix = point?.displayUnit && point.displayUnit !== '-' ? ` ${point.displayUnit}` : '';
                return [
                    `${activeParam?.axisValue || params?.[0]?.axisValue || '-'}`,
                    `数值: ${formatMetricValue(point?.value)}${suffix}`,
                    `来源: ${row?.source || '-'}`,
                    `发布时间: ${row?.published_at || '-'}`,
                ].join('<br/>');
            }
        },
        grid: {
            left: '3%',
            right: '4%',
            bottom: chartConfig.legendShow ? 110 : 90,
            top: chartConfig.legendShow ? 82 : 56,
            containLabel: true,
        },
        xAxis: {
            type: 'category',
            data: chartConfig.xAxisData,
            axisLabel: {
                rotate: chartConfig.xAxisLabelRotate ?? 45,
                fontSize: 10,
            }
        },
        yAxis: { type: 'value' },
        legend: {
            show: Boolean(chartConfig.legendShow),
            data: chartConfig.legendData || [],
            top: 34,
        },
        dataZoom: [
            {
                type: 'inside',
                start: startPercent,
                end: endPercent,
            },
            {
                type: 'slider',
                start: startPercent,
                end: endPercent,
                height: 20,
                bottom: 20,
                borderColor: '#D6DDE8',
                fillerColor: 'rgba(37, 99, 235, 0.16)',
                handleStyle: {
                    color: '#2563EB',
                    borderColor: '#1D4ED8',
                },
                moveHandleStyle: {
                    color: '#93C5FD',
                },
            }
        ],
        series: chartConfig.series
    });

    currentChart.off('datazoom');
    currentChart.on('datazoom', function() {
        syncVisibleRangeFromChart(totalPoints);
    });

    if (!chartResizeHandlerAttached) {
        window.addEventListener('resize', function() {
            if (currentChart) {
                currentChart.resize();
            }
        });
        chartResizeHandlerAttached = true;
    }

    syncVisibleRangeDependentUI();
}

function updateIndicatorGovernanceNote(payload) {
    const noteEl = document.getElementById('indicatorGovernanceNote');
    if (!noteEl) {
        return;
    }
    const governance = getPayloadGovernanceConfig(payload);
    if (!governance.note) {
        noteEl.textContent = '';
        noteEl.style.display = 'none';
        return;
    }
    noteEl.textContent = governance.note;
    noteEl.style.display = 'block';
}

function syncVisibleRangeFromChart(totalPoints) {
    if (!currentChart || totalPoints <= 0) {
        return;
    }
    const option = currentChart.getOption();
    const zoomOptions = Array.isArray(option.dataZoom) ? option.dataZoom : [];
    if (zoomOptions.length === 0) {
        return;
    }
    const zoomState = zoomOptions[0];
    const startPercent = Number(zoomState.start ?? 0);
    const endPercent = Number(zoomState.end ?? 100);
    const startIndex = totalPoints <= 1
        ? 0
        : Math.max(0, Math.min(totalPoints - 1, Math.floor((startPercent / 100) * (totalPoints - 1))));
    const endIndex = totalPoints <= 1
        ? 0
        : Math.max(startIndex, Math.min(totalPoints - 1, Math.ceil((endPercent / 100) * (totalPoints - 1))));
    const visibleCount = endIndex - startIndex + 1;
    const matchedPreset = RANGE_PRESET_OPTIONS.find(optionValue => {
        if (optionValue === 'all') {
            return startIndex === 0 && endIndex === totalPoints - 1;
        }
        return endIndex === totalPoints - 1 && visibleCount === Number(optionValue);
    }) || null;
    currentVisibleRange = {
        startIndex,
        endIndex,
        preset: matchedPreset,
    };
    syncVisibleRangeDependentUI();
}

function updateIndicatorInfo(code, payload) {
    if (!payload || !Array.isArray(payload.data) || payload.data.length === 0) {
        renderNoDataState(code);
        return;
    }
    const latest = payload.data[payload.data.length - 1];
    document.getElementById('currentIndicatorCode').textContent = code;
    document.getElementById('currentIndicatorName').textContent = payload.name_cn || indicatorNamesMap[code] || code;
    document.getElementById('currentIndicatorValue').textContent = formatMetricValue(latest.display_value ?? latest.value);
    document.getElementById('currentIndicatorUnit').textContent = latest.display_unit || latest.original_unit || latest.unit || '-';
    document.getElementById('currentIndicatorSource').textContent = latest.source || '-';
    document.getElementById('currentIndicatorPeriod').textContent = formatReportingPeriodLabel(latest.reporting_period, payload.period_type);
    document.getElementById('currentIndicatorDescription').textContent = payload.description || indicatorDescriptionsMap[code] || '';
    updateIndicatorGovernanceNote(payload);
    updateRefreshButtonState(code);
    updateVisibleRangeSummary();
}

function renderNoDataState(code) {
    currentIndicatorPayload = null;
    currentVisibleRange = { startIndex: 0, endIndex: -1, preset: '36' };
    detailTableScope = 'visible';
    const syncSupported = isIndicatorSyncSupported(code);
    const canRefresh = manualRefreshProviderId > 0 && syncSupported;
    showIndicatorInfoBar();
    document.getElementById('currentIndicatorCode').textContent = code;
    document.getElementById('currentIndicatorName').textContent = indicatorNamesMap[code] || code;
    document.getElementById('currentIndicatorValue').textContent = '-';
    document.getElementById('currentIndicatorUnit').textContent = indicatorUnitsMap[code] || '-';
    document.getElementById('currentIndicatorSource').textContent = '-';
    document.getElementById('currentIndicatorPeriod').textContent = '-';
    document.getElementById('currentIndicatorRecords').textContent = '0 条';
    document.getElementById('currentIndicatorDescription').textContent = indicatorDescriptionsMap[code] || '';
    updateIndicatorGovernanceNote(null);
    document.getElementById('currentRangeSummary').textContent = '暂无可展示区间';
    if (syncSupported) {
        showChartPlaceholder('该指标暂无同步数据', '指标目录已收口到 Data Center，但当前环境还没有该指标的宏观事实记录。');
    } else {
        showChartPlaceholder('该指标尚未接入自动同步', '该指标已在目录中展示，但尚未接入自动同步抓取链路，因此当前不能从页面直接补抓。');
    }
    document.getElementById('chartPlaceholderAction').innerHTML =
        canRefresh
            ? `<button type="button" class="placeholder-refresh-btn" onclick="refreshIndicator('${code}')">立即抓取当前指标</button>`
            : syncSupported
                ? '<div class="placeholder-note">当前未配置可用的 Data Center 宏观 provider</div>'
                : '<div class="placeholder-note">该指标尚未接入自动同步链路，请先在 Data Center 补齐接入配置。</div>';
    updateRangeControlsAvailability(false);
    updateDetailButtonState();
    renderDataDetailPanel();
    updateRefreshButtonState(code);
}

function toggleCategory(catId) {
    const content = document.getElementById(`cat-content-${catId}`);
    const icon = document.getElementById(`cat-icon-${catId}`);

    if (content.style.display === 'none') {
        content.style.display = 'block';
        icon.textContent = '▼';
    } else {
        content.style.display = 'none';
        icon.textContent = '▶';
    }
}

function collapseAllCategories() {
    document.querySelectorAll('.cat-content').forEach(el => {
        el.style.display = 'none';
    });
    document.querySelectorAll('.cat-toggle-icon').forEach(el => {
        el.textContent = '▶';
    });
}

function filterIndicators() {
    const searchTerm = document.getElementById('searchInput').value.toLowerCase();

    document.querySelectorAll('.cat-content').forEach(content => {
        const items = content.querySelectorAll('.ind-item');

        items.forEach(item => {
            const code = item.dataset.code.toLowerCase();
            const name = item.dataset.name.toLowerCase();
            item.style.display = code.includes(searchTerm) || name.includes(searchTerm) ? 'flex' : 'none';
        });
    });
}

function openDataDetailDrawer() {
    if (!currentIndicatorPayload || !Array.isArray(currentIndicatorPayload.data) || currentIndicatorPayload.data.length === 0) {
        return;
    }
    const drawer = document.getElementById('dataDetailDrawer');
    drawer.classList.add('open');
    drawer.setAttribute('aria-hidden', 'false');
    renderDataDetailPanel();
}

function closeDataDetailDrawer() {
    const drawer = document.getElementById('dataDetailDrawer');
    if (!drawer) {
        return;
    }
    drawer.classList.remove('open');
    drawer.setAttribute('aria-hidden', 'true');
}

function setDetailTableScope(scope) {
    detailTableScope = scope === 'all' ? 'all' : 'visible';
    renderDataDetailPanel();
}

function setDataDetailStatus(message, tone = 'muted') {
    const statusEl = document.getElementById('dataDetailCopyStatus');
    if (!statusEl) {
        return;
    }
    statusEl.textContent = message || '';
    statusEl.className = `detail-copy-status ${tone}`;
}

function buildMetaCard(label, valueHtml) {
    return `
        <div class="meta-card">
            <div class="meta-card-label">${escapeHtml(label)}</div>
            <div class="meta-card-value">${valueHtml}</div>
        </div>
    `;
}

function renderDataDetailPanel() {
    const countEl = document.getElementById('dataDetailCount');
    const metaEl = document.getElementById('dataDetailMeta');
    const bodyEl = document.getElementById('dataDetailTableBody');
    const emptyEl = document.getElementById('dataDetailEmpty');
    if (!countEl || !metaEl || !bodyEl || !emptyEl) {
        return;
    }

    document.querySelectorAll('.detail-scope-btn').forEach(button => {
        button.classList.toggle('active', button.dataset.scope === detailTableScope);
    });

    if (!currentIndicatorPayload || !Array.isArray(currentIndicatorPayload.data) || currentIndicatorPayload.data.length === 0) {
        countEl.textContent = '暂无记录';
        metaEl.innerHTML = '';
        bodyEl.innerHTML = '';
        emptyEl.style.display = 'block';
        emptyEl.textContent = '当前指标没有可展示的明细数据。';
        setDataDetailStatus('', 'muted');
        return;
    }

    const visibleRows = getVisibleRows();
    const scopedRows = detailTableScope === 'all' ? currentIndicatorPayload.data : visibleRows;
    const latest = currentIndicatorPayload.data[currentIndicatorPayload.data.length - 1];
    countEl.textContent = detailTableScope === 'all'
        ? `全部 ${currentIndicatorPayload.data.length} 条记录`
        : `当前区间 ${scopedRows.length} 条记录 · ${buildVisibleRangeLabel()}`;
    metaEl.innerHTML = [
        buildMetaCard('指标', `${escapeHtml(currentIndicatorPayload.name_cn || currentIndicatorPayload.indicator_code)}<span class="meta-card-sub">${escapeHtml(currentIndicatorPayload.indicator_code)}</span>`),
        buildMetaCard('最新值', `${escapeHtml(formatMetricValue(latest.display_value ?? latest.value))} ${escapeHtml(latest.display_unit || latest.original_unit || latest.unit || '-')}`),
        buildMetaCard('最新报告期', escapeHtml(formatReportingPeriodLabel(latest.reporting_period, currentIndicatorPayload.period_type))),
        buildMetaCard('数据来源', escapeHtml(currentIndicatorPayload.publisher || latest.source || currentIndicatorPayload.data_source || '-')),
        buildMetaCard('发布渠道', escapeHtml(currentIndicatorPayload.access_channel || '-')),
        buildMetaCard('新鲜度', escapeHtml(currentIndicatorPayload.freshness_status || latest.freshness_status || '-')),
        buildMetaCard('决策等级', escapeHtml(currentIndicatorPayload.decision_grade || latest.decision_grade || '-')),
        buildMetaCard('时间语义', escapeHtml(currentIndicatorPayload.series_semantics || currentIndicatorPayload.period_type || '-')),
    ].join('');

    if (scopedRows.length === 0) {
        bodyEl.innerHTML = '';
        emptyEl.style.display = 'block';
        emptyEl.textContent = '当前区间没有数据，请拖动滑块或切换到“全部记录”。';
        return;
    }

    bodyEl.innerHTML = scopedRows.map(row => `
        <tr>
            <td>${escapeHtml(formatReportingPeriodLabel(row.reporting_period, currentIndicatorPayload.period_type))}</td>
            <td class="number-cell">${escapeHtml(formatMetricValue(row.display_value ?? row.value))}</td>
            <td>${escapeHtml(row.display_unit || row.original_unit || row.unit || '-')}</td>
            <td class="number-cell">${escapeHtml(formatMetricValue(row.value))}</td>
            <td>${escapeHtml(row.unit || '-')}</td>
            <td>${escapeHtml(row.original_unit || '-')}</td>
            <td>${escapeHtml(row.source || '-')}</td>
            <td>${escapeHtml(formatDisplayDate(row.published_at))}</td>
            <td>${escapeHtml(row.quality || '-')}</td>
            <td>${escapeHtml(row.freshness_status || '-')}</td>
            <td class="number-cell">${escapeHtml(row.age_days ?? '-')}</td>
        </tr>
    `).join('');
    emptyEl.style.display = 'none';
}

function buildMarkdownForScope(scope) {
    if (!currentIndicatorPayload || !Array.isArray(currentIndicatorPayload.data) || currentIndicatorPayload.data.length === 0) {
        return '';
    }
    const rows = scope === 'all' ? currentIndicatorPayload.data : getVisibleRows();
    const latest = currentIndicatorPayload.data[currentIndicatorPayload.data.length - 1];
    const summaryRows = [
        ['指标代码', currentIndicatorPayload.indicator_code],
        ['指标名称', currentIndicatorPayload.name_cn || '-'],
        ['描述', currentIndicatorPayload.description || '-'],
        ['时间语义', currentIndicatorPayload.series_semantics || currentIndicatorPayload.period_type || '-'],
        ['当前显示区间', buildVisibleRangeLabel()],
        ['导出范围', scope === 'all' ? `全部 ${currentIndicatorPayload.data.length} 条` : `当前区间 ${rows.length} 条`],
        ['最新值', `${formatMetricValue(latest.display_value ?? latest.value)} ${latest.display_unit || latest.original_unit || latest.unit || '-'}`],
        ['最新报告期', formatReportingPeriodLabel(latest.reporting_period, currentIndicatorPayload.period_type)],
        ['发布时间', currentIndicatorPayload.latest_published_at || latest.published_at || '-'],
        ['数据来源', currentIndicatorPayload.publisher || latest.source || currentIndicatorPayload.data_source || '-'],
        ['发布渠道', currentIndicatorPayload.access_channel || '-'],
        ['新鲜度', currentIndicatorPayload.freshness_status || latest.freshness_status || '-'],
        ['决策等级', currentIndicatorPayload.decision_grade || latest.decision_grade || '-'],
        ['阻断原因', currentIndicatorPayload.blocked_reason || '-'],
    ];

    const lines = [
        `## ${currentIndicatorPayload.name_cn || currentIndicatorPayload.indicator_code} (${currentIndicatorPayload.indicator_code})`,
        '',
        '| 字段 | 值 |',
        '| --- | --- |',
    ];
    summaryRows.forEach(row => {
        lines.push(`| ${escapeMarkdownCell(row[0])} | ${escapeMarkdownCell(row[1])} |`);
    });

    lines.push(
        '',
        '| 报告期 | 展示值 | 展示单位 | 存储值 | 存储单位 | 原始单位 | 数据源 | 发布时间 | 质量 | 新鲜度 | 天龄 |',
        '| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |',
    );

    rows.forEach(row => {
        lines.push(
            `| ${escapeMarkdownCell(formatReportingPeriodLabel(row.reporting_period, currentIndicatorPayload.period_type))}`
            + ` | ${escapeMarkdownCell(formatMetricValue(row.display_value ?? row.value))}`
            + ` | ${escapeMarkdownCell(row.display_unit || row.original_unit || row.unit || '-')}`
            + ` | ${escapeMarkdownCell(formatMetricValue(row.value))}`
            + ` | ${escapeMarkdownCell(row.unit || '-')}`
            + ` | ${escapeMarkdownCell(row.original_unit || '-')}`
            + ` | ${escapeMarkdownCell(row.source || '-')}`
            + ` | ${escapeMarkdownCell(formatDisplayDate(row.published_at))}`
            + ` | ${escapeMarkdownCell(row.quality || '-')}`
            + ` | ${escapeMarkdownCell(row.freshness_status || '-')}`
            + ` | ${escapeMarkdownCell(row.age_days ?? '-')}`
            + ' |'
        );
    });

    return lines.join('\n');
}

async function copyDataDetailMarkdown(scope) {
    const markdown = buildMarkdownForScope(scope);
    if (!markdown) {
        setDataDetailStatus('当前没有可复制的数据。', 'warning');
        return;
    }

    try {
        if (navigator.clipboard && window.isSecureContext) {
            await navigator.clipboard.writeText(markdown);
        } else {
            const textarea = document.createElement('textarea');
            textarea.value = markdown;
            textarea.setAttribute('readonly', 'readonly');
            textarea.style.position = 'absolute';
            textarea.style.left = '-9999px';
            document.body.appendChild(textarea);
            textarea.select();
            document.execCommand('copy');
            document.body.removeChild(textarea);
        }
        setDataDetailStatus(scope === 'all' ? '已复制全部记录 Markdown。' : '已复制当前区间 Markdown。', 'success');
    } catch (error) {
        setDataDetailStatus('复制失败，请手动重试。', 'error');
    }
}

function switchTab(tabName) {
    document.querySelectorAll('.tab-button').forEach(btn => {
        btn.classList.remove('active');
        if (btn.dataset.tab === tabName) {
            btn.classList.add('active');
        }
    });

    document.querySelectorAll('.tab-content').forEach(content => {
        content.classList.remove('active');
    });
    document.getElementById(`tab-${tabName}`).classList.add('active');
}

async function askAI(question) {
    document.getElementById('aiQuestion').value = question;
    submitAIQuestion();
}

async function submitAIQuestion() {
    const question = document.getElementById('aiQuestion').value.trim();
    if (!question) {
        return;
    }

    const responseDiv = document.getElementById('aiResponse');
    const statusSpan = document.getElementById('aiStatus');
    const submitBtn = document.querySelector('.ai-submit-btn');

    statusSpan.textContent = '分析中...';
    responseDiv.innerHTML = '<div class="ai-placeholder"><div class="placeholder-icon">⏳</div><p>AI 正在分析...</p></div>';
    submitBtn.disabled = true;

    const context = {
        has_indicator: !!currentIndicatorCode,
    };
    if (currentIndicatorCode) {
        context.indicator_code = currentIndicatorCode;
        context.indicator_name = indicatorNamesMap[currentIndicatorCode] || currentIndicatorCode;
        context.indicator_value = document.getElementById('currentIndicatorValue').textContent;
    }

    try {
        const response = await fetch('/api/prompt/chat', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRFToken': getCookie('csrftoken')
            },
            body: JSON.stringify({
                message: question,
                context: context
            })
        });

        const data = await response.json();

        if (data.reply) {
            responseDiv.innerHTML = `<div class="ai-response-content">${data.reply}</div>`;
            statusSpan.textContent = '';
        } else {
            throw new Error('未收到回复');
        }
    } catch (error) {
        responseDiv.innerHTML = '<div class="ai-response-content">AI 暂时不可用，请稍后重试。</div>';
        statusSpan.textContent = '(离线)';
    }

    submitBtn.disabled = false;
}

function getCookie(name) {
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
            const cookie = cookies[i].trim();
            if (cookie.substring(0, name.length + 1) === `${name}=`) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
}
</script>

<!-- 统计卡片 -->
<div class="stats-row">
    <div class="stat-card">
        <div class="stat-icon">📊</div>
        <div class="stat-info">
            <div class="stat-label">指标种类</div>
            <div class="stat-value">{{ stats.total_indicators }}</div>
        </div>
    </div>
    <div class="stat-card">
        <div class="stat-icon">📈</div>
        <div class="stat-info">
            <div class="stat-label">数据记录</div>
            <div class="stat-value">{{ stats.total_records }}</div>
        </div>
    </div>
    <div class="stat-card">
        <div class="stat-icon">🔄</div>
        <div class="stat-info">
            <div class="stat-label">可自动抓取</div>
            <div class="stat-value">{{ sync_supported_indicator_count }}</div>
        </div>
    </div>
    <div class="stat-card">
        <div class="stat-icon">📅</div>
        <div class="stat-info">
            <div class="stat-label">最新更新</div>
            <div class="stat-value stat-date">
                {% if stats.latest_date != '-' %}
                    {{ stats.latest_date|date:"Y-m-d" }}
                {% else %}
                    -
                {% endif %}
            </div>
        </div>
    </div>
</div>

{% with mt=market_thermometer %}
{% if mt %}
<div class="macro-thermometer-row">
    {% include "components/market_thermometer_card.html" with market_temperature_observed_at=mt.market_temperature_observed_at market_temperature_score=mt.market_temperature_score market_temperature_band=mt.market_temperature_band market_temperature_band_label=mt.market_temperature_band_label market_temperature_change_5d=mt.market_temperature_change_5d market_temperature_change_20d=mt.market_temperature_change_20d market_temperature_change_5d_arrow=mt.market_temperature_change_5d_arrow market_temperature_change_20d_arrow=mt.market_temperature_change_20d_arrow market_temperature_is_hot=mt.market_temperature_is_hot market_temperature_is_overheat=mt.market_temperature_is_overheat market_temperature_threshold_source=mt.market_temperature_threshold_source market_temperature_components=mt.market_temperature_components market_temperature_top_reasons=mt.market_temperature_top_reasons market_temperature_degraded=mt.market_temperature_degraded market_temperature_blocked_reason=mt.market_temperature_blocked_reason only %}
</div>
{% endif %}
{% endwith %}

<div class="macro-dashboard">
    <!-- 左侧边栏 -->
    <div class="left-sidebar">
        <div class="sidebar-card">
            <div class="search-section">
                <input type="text" id="searchInput" value="{{ filter_search }}"
                       placeholder="搜索指标名称或代码..."
                       class="search-input"
                       oninput="filterIndicators()">
            </div>
        </div>

        <div class="sidebar-card sidebar-card-list">
            <div class="sidebar-header">
                <h3>指标列表</h3>
            </div>
            <div class="categories-container">
                {% for category in indicator_categories %}
                <div class="category-block">
                    <div class="category-header" onclick="toggleCategory('{{ category.id }}')">
                        <span class="cat-toggle-icon" id="cat-icon-{{ category.id }}">{% if category.is_selected_category %}▼{% else %}▶{% endif %}</span>
                        <span class="cat-icon">{{ category.icon }}</span>
                        <span class="cat-name">{{ category.name }}</span>
                        <span class="cat-count">({{ category.indicators|length }})</span>
                    </div>
                    <div class="cat-content" id="cat-content-{{ category.id }}" {% if category.is_selected_category %}style="display:block;"{% else %}style="display:none;"{% endif %}>
                        {% for ind in category.indicators %}
                        <div class="ind-item {% if selected_indicator == ind.code %}active{% endif %}"
                             data-code="{{ ind.code }}"
                             data-name="{{ ind.name }}"
                             data-unit="{{ ind.unit|default:'-' }}"
                             data-has-data="{{ ind.has_data|yesno:'true,false' }}"
                             data-sync-supported="{{ ind.sync_supported|yesno:'true,false' }}"
                             data-refresh-start="{{ ind.refresh_start|date:'Y-m-d' }}"
                             onclick="selectIndicator('{{ ind.code }}')">
                            <span class="ind-code">{{ ind.code }}</span>
                            <span class="ind-name">{{ ind.name }}</span>
                            {% if not ind.has_data %}
                            <span class="ind-status-badge">{% if ind.sync_supported %}未同步{% else %}未接入{% endif %}</span>
                            {% endif %}
                        </div>
                        {% endfor %}
                    </div>
                </div>
                {% endfor %}
            </div>
        </div>
    </div>

    <!-- 中间：图表区域 -->
    <div class="main-panel">
        <!-- 当前指标信息 -->
        <div id="indicatorInfoBar" class="indicator-info-bar" style="display: none;">
            <div class="info-bar-section">
                <span class="info-label">指标:</span>
                <span class="info-value" id="currentIndicatorCode">-</span>
            </div>
            <div class="info-bar-section">
                <span class="info-label">名称:</span>
                <span class="info-value" id="currentIndicatorName">-</span>
            </div>
            <div class="info-bar-section">
                <span class="info-label">最新值:</span>
                <span class="info-value highlight" id="currentIndicatorValue">-</span>
                <span class="info-value" id="currentIndicatorUnit" style="font-size: 14px; margin-left: 4px;">-</span>
            </div>
            <div class="info-bar-section">
                <span class="info-label">日期:</span>
                <span class="info-value" id="currentIndicatorPeriod">-</span>
            </div>
            <div class="info-bar-section">
                <span class="info-label">数据源:</span>
                <span class="info-value" id="currentIndicatorSource">-</span>
            </div>
            <div class="info-bar-section">
                <span class="info-label">记录数:</span>
                <span class="info-value" id="currentIndicatorRecords">-</span>
            </div>
            <div class="info-bar-actions">
                <button id="refreshIndicatorBtn" type="button" class="refresh-indicator-btn" onclick="refreshIndicator()">
                    手动刷新抓取
                </button>
                <span id="refreshIndicatorStatus" class="refresh-status muted" aria-live="polite"></span>
            </div>
        </div>
        <div class="indicator-description" id="currentIndicatorDescription"></div>
        <div class="indicator-governance-note" id="indicatorGovernanceNote" style="display: none;"></div>

        <div class="chart-toolbar">
            <div class="chart-range-controls">
                <span class="chart-toolbar-label">显示区间</span>
                <button type="button" class="range-preset-btn" data-range="12" onclick="applyRangePreset('12')">近12</button>
                <button type="button" class="range-preset-btn" data-range="24" onclick="applyRangePreset('24')">近24</button>
                <button type="button" class="range-preset-btn" data-range="36" onclick="applyRangePreset('36')">近36</button>
                <button type="button" class="range-preset-btn" data-range="60" onclick="applyRangePreset('60')">近60</button>
                <button type="button" class="range-preset-btn" data-range="all" onclick="applyRangePreset('all')">全部</button>
            </div>
            <div class="chart-toolbar-actions">
                <span id="currentRangeSummary" class="current-range-summary">暂无可展示区间</span>
                <button id="openDataDetailBtn" type="button" class="detail-drawer-btn" onclick="openDataDetailDrawer()">
                    表格详情 / Markdown
                </button>
            </div>
        </div>

        <!-- 主图表 -->
        <div class="main-chart-container">
            <div id="mainChart" class="main-chart"></div>
        </div>

        <!-- 数据提示 -->
        <div id="chartPlaceholder" class="chart-placeholder">
            <div class="placeholder-icon">📈</div>
            <div class="placeholder-title" id="chartPlaceholderTitle">选择一个指标查看趋势</div>
            <div class="placeholder-desc" id="chartPlaceholderDesc">从左侧列表点击指标名称</div>
            <div id="chartPlaceholderAction" class="placeholder-action"></div>
        </div>
    </div>

    <!-- 右侧：AI 分析 -->
    <div class="right-sidebar">
        <div class="ai-panel">
            <div class="panel-header">
                <h3>🤖 AI 分析与解读</h3>
            </div>

            <div class="ai-content">
                <div class="ai-input-section">
                    <div class="quick-questions">
                        <button class="quick-btn" onclick="askAI('解释当前指标的趋势变化特征')">趋势特征</button>
                        <button class="quick-btn" onclick="askAI('分析这个指标的周期性波动')">周期分析</button>
                        <button class="quick-btn" onclick="askAI('当前处于上升还是下降趋势？')">趋势判断</button>
                        <button class="quick-btn" onclick="askAI('这个指标的异常波动有哪些？')">异常分析</button>
                    </div>
                    <textarea id="aiQuestion" class="ai-textarea" placeholder="输入您的问题，或点击上方快捷按钮..."></textarea>
                    <button class="ai-submit-btn" onclick="submitAIQuestion()">
                        <span class="btn-icon">🤖</span>
                        分析
                    </button>
                </div>

                <div class="ai-output-section">
                    <div class="output-header">
                        <span class="output-title">分析结果</span>
                        <span class="output-status" id="aiStatus"></span>
                    </div>
                    <div id="aiResponse" class="ai-response">
                        <div class="ai-placeholder">
                            <div class="placeholder-icon">💭</div>
                            <p>选择指标后，可让 AI 分析趋势特征</p>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<div id="dataDetailDrawer" class="data-detail-drawer" aria-hidden="true">
    <div class="data-detail-backdrop" onclick="closeDataDetailDrawer()"></div>
    <aside class="data-detail-panel" role="dialog" aria-modal="true" aria-labelledby="dataDetailTitle">
        <div class="data-detail-header">
            <div>
                <h3 id="dataDetailTitle">指标明细</h3>
                <p id="dataDetailCount" class="data-detail-count">暂无记录</p>
            </div>
            <button type="button" class="data-detail-close" onclick="closeDataDetailDrawer()" aria-label="关闭">×</button>
        </div>

        <div id="dataDetailMeta" class="data-detail-meta"></div>

        <div class="data-detail-actions">
            <div class="detail-scope-switch">
                <button type="button" class="detail-scope-btn active" data-scope="visible" onclick="setDetailTableScope('visible')">当前区间</button>
                <button type="button" class="detail-scope-btn" data-scope="all" onclick="setDetailTableScope('all')">全部记录</button>
            </div>
            <div class="detail-copy-actions">
                <button type="button" class="detail-copy-btn" onclick="copyDataDetailMarkdown('visible')">复制当前区间 Markdown</button>
                <button type="button" class="detail-copy-btn secondary" onclick="copyDataDetailMarkdown('all')">复制全部 Markdown</button>
            </div>
        </div>

        <div id="dataDetailCopyStatus" class="detail-copy-status muted" aria-live="polite"></div>

        <div class="data-detail-table-wrap">
            <table class="data-detail-table">
                <thead>
                    <tr>
                        <th>报告期</th>
                        <th>展示值</th>
                        <th>展示单位</th>
                        <th>存储值</th>
                        <th>存储单位</th>
                        <th>原始单位</th>
                        <th>数据源</th>
                        <th>发布时间</th>
                        <th>质量</th>
                        <th>新鲜度</th>
                        <th>天龄</th>
                    </tr>
                </thead>
                <tbody id="dataDetailTableBody"></tbody>
            </table>
            <div id="dataDetailEmpty" class="data-detail-empty">当前指标没有可展示的明细数据。</div>
        </div>
    </aside>
</div>

<style>
/* 页面头部 */
.header-content {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
}

.header-controls {
    display: flex;
    align-items: center;
    gap: 12px;
}

.control-btn {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 8px 16px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: white;
    color: var(--color-text-primary);
    font-size: 14px;
    cursor: pointer;
    transition: all 0.2s;
}

.control-btn:hover {
    border-color: #2563EB;
    background: #F6F8FA;
}

.control-btn:disabled {
    opacity: 0.55;
    cursor: not-allowed;
    border-color: var(--color-border);
    background: white;
}

/* 统计卡片 */
.stats-row {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 16px;
    margin-bottom: 20px;
}

.macro-thermometer-row {
    margin-bottom: 20px;
}

.stat-card {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 20px;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
}

.stat-icon {
    font-size: 32px;
    opacity: 0.8;
}

.stat-info {
    flex: 1;
}

.stat-label {
    font-size: 12px;
    color: var(--color-text-secondary);
    margin-bottom: 4px;
}

.stat-value {
    font-size: 24px;
    font-weight: 700;
    color: #2563EB;
}

.stat-value.stat-date {
    font-size: 14px;
}

.pulse-dashboard-card {
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    padding: 20px;
}

.pulse-card__header,
.pulse-composite {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
}

.pulse-card__header {
    margin-bottom: 16px;
}

.pulse-card__title {
    margin: 0;
    font-size: 18px;
    font-weight: 700;
    color: var(--color-text-primary);
}

.pulse-card__date {
    font-size: 12px;
    color: var(--color-text-secondary);
}

.pulse-composite__score {
    min-width: 88px;
    padding: 10px 14px;
    border-radius: 8px;
    font-size: 32px;
    font-weight: 700;
    text-align: center;
}

.pulse-composite--weak {
    background: rgba(14, 165, 233, 0.12);
    color: #0369A1;
}

.pulse-composite--moderate {
    background: rgba(245, 158, 11, 0.12);
    color: #B45309;
}

.pulse-composite--strong {
    background: rgba(239, 68, 68, 0.12);
    color: #B91C1C;
}

.pulse-composite__label {
    flex: 1;
    font-size: 15px;
    font-weight: 600;
    color: var(--color-text-primary);
    text-align: right;
}

.pulse-warning {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border-radius: 8px;
    background: rgba(245, 158, 11, 0.12);
    color: #92400E;
}

.pulse-warning__text {
    font-size: 13px;
}

.pulse-transition-reasons {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
}

.pulse-dimensions {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 12px;
}

.pulse-dim {
    padding: 12px;
    border: 1px solid var(--color-border);
    border-radius: 8px;
    background: var(--color-surface);
}

.pulse-dim__label,
.pulse-dim__score {
    font-size: 12px;
    color: var(--color-text-secondary);
}

.pulse-dim__score {
    margin-top: 6px;
    margin-bottom: 8px;
}

.pulse-dim__bar {
    height: 8px;
    background: rgba(148, 163, 184, 0.18);
    border-radius: 999px;
    overflow: hidden;
}

.pulse-dim__fill {
    height: 100%;
    border-radius: 999px;
}

.pulse-stale-notice {
    margin-top: 12px;
    padding: 10px 12px;
    border-radius: 8px;
    background: rgba(239, 68, 68, 0.08);
    color: #991B1B;
    font-size: 13px;
}

/* 主布局 */
.macro-dashboard {
    display: grid;
    grid-template-columns: 260px 1fr 360px;
    gap: 20px;
    align-items: start;
}

/* 左侧边栏 */
.left-sidebar {
    display: flex;
    flex-direction: column;
    gap: 16px;
    position: sticky;
    top: 20px;
    height: calc(100vh - 140px);
    max-height: calc(100vh - 140px);
    min-height: 0;
    overflow: hidden;
}

.sidebar-card {
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    overflow: hidden;
}

.sidebar-card-list {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
}

.search-section {
    padding: 12px;
}

.search-input {
    width: 100%;
    padding: 10px 12px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    font-size: 13px;
    box-sizing: border-box;
}

.search-input:focus {
    outline: none;
    border-color: #2563EB;
    box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);
}

.sidebar-header {
    padding: 12px 16px;
    border-bottom: 1px solid var(--color-border);
    background: var(--color-surface);
}

.sidebar-header h3 {
    font-size: 13px;
    font-weight: 600;
    color: var(--color-text-primary);
    margin: 0;
}

.categories-container {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
}

.category-block {
    border-bottom: 1px solid var(--color-surface);
}

.category-block:last-child {
    border-bottom: none;
}

.category-header {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 12px 16px;
    cursor: pointer;
    transition: background 0.2s;
    user-select: none;
}

.category-header:hover {
    background: var(--color-surface);
}

.cat-toggle-icon {
    font-size: 10px;
    color: var(--color-text-muted);
    width: 14px;
}

.cat-icon {
    font-size: 14px;
}

.cat-name {
    flex: 1;
    font-size: 13px;
    font-weight: 500;
    color: var(--color-text-primary);
}

.cat-count {
    font-size: 11px;
    color: var(--color-text-muted);
}

.cat-content {
    background: #fafafa;
}

.ind-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 16px 8px 26px;
    cursor: pointer;
    transition: background 0.2s;
    border-left: 2px solid transparent;
}

.ind-item:hover {
    background: white;
}

.ind-item.active {
    background: #EFF6FF;
    border-left-color: #2563EB;
}

.ind-code {
    font-size: 11px;
    font-weight: 600;
    font-family: monospace;
    color: #2563EB;
    min-width: 80px;
}

.ind-name {
    flex: 1;
    font-size: 12px;
    color: var(--color-text-primary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.ind-status-badge {
    flex-shrink: 0;
    padding: 2px 6px;
    border-radius: 999px;
    background: #FEF3C7;
    color: #92400E;
    font-size: 10px;
    font-weight: 600;
}

/* 中间面板 */
.main-panel {
    display: flex;
    flex-direction: column;
    gap: 16px;
}

.indicator-info-bar {
    display: flex;
    flex-wrap: wrap;
    gap: 16px 24px;
    align-items: flex-end;
    padding: 16px;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
}

.info-bar-section {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.info-label {
    font-size: 11px;
    color: var(--color-text-secondary);
    font-weight: 500;
}

.info-value {
    font-size: 14px;
    color: var(--color-text-primary);
    font-weight: 500;
}

.info-value.highlight {
    font-size: 18px;
    font-weight: 700;
    color: #2563EB;
}

.info-bar-actions {
    margin-left: auto;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 8px;
}

.refresh-indicator-btn,
.placeholder-refresh-btn {
    padding: 9px 14px;
    border: 1px solid #2563EB;
    border-radius: var(--radius-sm);
    background: white;
    color: #2563EB;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.2s;
}

.refresh-indicator-btn:hover,
.placeholder-refresh-btn:hover {
    background: #EFF6FF;
}

.refresh-indicator-btn:disabled,
.placeholder-refresh-btn:disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

.refresh-status {
    min-height: 18px;
    font-size: 12px;
}

.refresh-status.muted {
    color: var(--color-text-muted);
}

.refresh-status.success {
    color: #166534;
}

.refresh-status.warning {
    color: #92400E;
}

.refresh-status.error {
    color: #B91C1C;
}

.main-chart-container {
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    padding: 20px;
    min-height: 500px;
}

.indicator-description {
    margin: 10px 0 14px;
    color: var(--color-text-secondary);
    font-size: 13px;
    line-height: 1.5;
}

.chart-toolbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 16px;
    flex-wrap: wrap;
    padding: 14px 16px;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
}

.chart-range-controls,
.chart-toolbar-actions {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}

.chart-toolbar-label {
    font-size: 12px;
    color: var(--color-text-secondary);
    font-weight: 600;
}

.range-preset-btn,
.detail-drawer-btn,
.detail-scope-btn,
.detail-copy-btn {
    padding: 8px 12px;
    border: 1px solid var(--color-border);
    border-radius: 999px;
    background: white;
    color: var(--color-text-primary);
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.2s;
}

.range-preset-btn:hover,
.detail-drawer-btn:hover,
.detail-scope-btn:hover,
.detail-copy-btn:hover {
    border-color: #2563EB;
    color: #2563EB;
    background: #EFF6FF;
}

.range-preset-btn.active,
.detail-scope-btn.active {
    background: #2563EB;
    border-color: #2563EB;
    color: white;
}

.range-preset-btn:disabled,
.detail-drawer-btn:disabled,
.detail-scope-btn:disabled,
.detail-copy-btn:disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

.detail-drawer-btn {
    border-radius: var(--radius-sm);
}

.current-range-summary {
    font-size: 12px;
    color: var(--color-text-secondary);
}

.main-chart {
    width: 100%;
    height: 480px;
}

.chart-placeholder {
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    padding: 60px 20px;
    min-height: 500px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
}

.placeholder-icon {
    font-size: 64px;
    margin-bottom: 16px;
    opacity: 0.5;
}

.placeholder-title {
    font-size: 18px;
    font-weight: 600;
    color: var(--color-text-primary);
    margin-bottom: 8px;
}

.placeholder-desc {
    font-size: 14px;
    color: var(--color-text-muted);
}

.placeholder-action {
    margin-top: 18px;
}

.placeholder-note {
    font-size: 12px;
    color: var(--color-text-muted);
}

.data-detail-drawer {
    position: fixed;
    inset: 0;
    z-index: 1200;
    pointer-events: none;
}

.data-detail-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(15, 23, 42, 0.35);
    opacity: 0;
    transition: opacity 0.2s ease;
}

.data-detail-panel {
    position: absolute;
    top: 0;
    right: 0;
    width: min(860px, 100vw);
    height: 100%;
    background: #ffffff;
    box-shadow: -16px 0 48px rgba(15, 23, 42, 0.16);
    transform: translateX(100%);
    transition: transform 0.24s ease;
    display: flex;
    flex-direction: column;
}

.data-detail-drawer.open {
    pointer-events: auto;
}

.data-detail-drawer.open .data-detail-backdrop {
    opacity: 1;
}

.data-detail-drawer.open .data-detail-panel {
    transform: translateX(0);
}

.data-detail-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: 16px;
    padding: 20px 24px 12px;
    border-bottom: 1px solid var(--color-border);
}

.data-detail-header h3 {
    margin: 0 0 6px;
    font-size: 20px;
    color: var(--color-text-primary);
}

.data-detail-count {
    margin: 0;
    font-size: 13px;
    color: var(--color-text-secondary);
}

.data-detail-close {
    border: none;
    background: transparent;
    color: var(--color-text-muted);
    font-size: 28px;
    line-height: 1;
    cursor: pointer;
    padding: 0;
}

.data-detail-meta {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: 12px;
    padding: 16px 24px;
}

.meta-card {
    padding: 12px 14px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: #FAFBFC;
}

.meta-card-label {
    font-size: 11px;
    color: var(--color-text-secondary);
    margin-bottom: 6px;
    font-weight: 600;
}

.meta-card-value {
    font-size: 13px;
    color: var(--color-text-primary);
    font-weight: 600;
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.meta-card-sub {
    font-size: 11px;
    color: var(--color-text-muted);
    font-weight: 500;
}

.data-detail-actions {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    padding: 0 24px 12px;
    flex-wrap: wrap;
}

.detail-scope-switch,
.detail-copy-actions {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
}

.detail-copy-btn.secondary {
    border-style: dashed;
}

.detail-copy-status {
    min-height: 18px;
    padding: 0 24px 12px;
    font-size: 12px;
}

.detail-copy-status.muted {
    color: var(--color-text-muted);
}

.detail-copy-status.success {
    color: #166534;
}

.detail-copy-status.warning {
    color: #92400E;
}

.detail-copy-status.error {
    color: #B91C1C;
}

.data-detail-table-wrap {
    flex: 1;
    overflow: auto;
    padding: 0 24px 24px;
}

.data-detail-table {
    width: 100%;
    min-width: 980px;
    border-collapse: collapse;
    background: white;
}

.data-detail-table th,
.data-detail-table td {
    padding: 10px 12px;
    border-bottom: 1px solid var(--color-border);
    font-size: 12px;
    text-align: left;
    vertical-align: top;
}

.data-detail-table th {
    position: sticky;
    top: 0;
    background: #F8FAFC;
    z-index: 1;
    font-weight: 700;
    color: var(--color-text-primary);
}

.data-detail-table .number-cell {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

.data-detail-empty {
    display: none;
    padding: 32px 8px;
    text-align: center;
    color: var(--color-text-muted);
    font-size: 13px;
}

/* 右侧边栏 */
.right-sidebar {
    position: sticky;
    top: 20px;
}

.ai-panel {
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    overflow: hidden;
    max-height: calc(100vh - 140px);
}

.panel-header {
    padding: 16px;
    border-bottom: 1px solid var(--color-border);
    background: var(--color-surface);
}

.panel-header h3 {
    font-size: 14px;
    font-weight: 600;
    color: var(--color-text-primary);
    margin: 0;
}

.ai-content {
    display: flex;
    flex-direction: column;
    height: calc(100vh - 180px);
}

.ai-input-section {
    padding: 16px;
    border-bottom: 1px solid var(--color-border);
}

.quick-questions {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-bottom: 12px;
}

.quick-btn {
    padding: 6px 10px;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    font-size: 11px;
    color: var(--color-text-secondary);
    cursor: pointer;
    transition: all 0.2s;
}

.quick-btn:hover {
    border-color: #2563EB;
    background: rgba(37, 99, 235, 0.05);
    color: #2563EB;
}

.ai-textarea {
    width: 100%;
    min-height: 60px;
    padding: 10px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    font-size: 13px;
    font-family: inherit;
    resize: vertical;
    margin-bottom: 10px;
    box-sizing: border-box;
}

.ai-textarea:focus {
    outline: none;
    border-color: #2563EB;
    box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);
}

.ai-submit-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    width: 100%;
    padding: 10px;
    background: #2563EB;
    color: white;
    border: none;
    border-radius: var(--radius-sm);
    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    transition: all 0.2s;
}

.ai-submit-btn:hover {
    background: #1D4ED8;
}

.ai-submit-btn:disabled {
    opacity: 0.6;
    cursor: not-allowed;
}

.ai-output-section {
    padding: 16px;
    flex: 1;
    overflow-y: auto;
}

.output-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 12px;
}

.output-title {
    font-size: 13px;
    font-weight: 600;
    color: var(--color-text-primary);
}

.output-status {
    font-size: 11px;
    color: var(--color-text-muted);
}

.ai-response {
    min-height: 100px;
}

.ai-placeholder {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 40px 20px;
    text-align: center;
    color: var(--color-text-muted);
}

.ai-response-content {
    font-size: 13px;
    line-height: 1.6;
    color: var(--color-text-primary);
    white-space: pre-wrap;
}

/* 响应式 */
@media (max-width: 1400px) {
    .macro-dashboard {
        grid-template-columns: 240px 1fr 320px;
    }
}

@media (max-width: 1200px) {
    .macro-dashboard {
        grid-template-columns: 1fr;
    }

    .left-sidebar, .right-sidebar {
        position: static;
        height: auto;
        max-height: none;
    }

    .categories-container {
        max-height: none;
    }

    .stats-row {
        grid-template-columns: 1fr;
    }

    .pulse-composite,
    .pulse-card__header {
        flex-direction: column;
        align-items: flex-start;
    }

    .pulse-composite__label {
        text-align: left;
    }

    .pulse-dimensions {
        grid-template-columns: 1fr;
    }

    .data-detail-panel {
        width: 100vw;
    }
}

@media (max-width: 768px) {
    .indicator-info-bar {
        flex-direction: column;
        gap: 12px;
    }

    .info-bar-actions {
        margin-left: 0;
        align-items: stretch;
        width: 100%;
    }

    .main-chart {
        height: 300px;
    }

    .chart-placeholder {
        min-height: 300px;
    }

    .chart-toolbar,
    .data-detail-actions {
        flex-direction: column;
        align-items: stretch;
    }

    .chart-toolbar-actions,
    .chart-range-controls,
    .detail-scope-switch,
    .detail-copy-actions {
        width: 100%;
    }

    .range-preset-btn,
    .detail-drawer-btn,
    .detail-scope-btn,
    .detail-copy-btn {
        flex: 1 1 auto;
        justify-content: center;
    }

    .data-detail-header,
    .data-detail-meta,
    .data-detail-actions,
    .detail-copy-status,
    .data-detail-table-wrap {
        padding-left: 16px;
        padding-right: 16px;
    }
}
</style>

{% load static %}
<script src="{% static 'vendor/js/echarts.min.js' %}"></script>
{% endblock %}
