1<!doctype html>
1<script type="application/json" id="cowork-artifact-meta"> 2{ 3 "name": "Energoreport", 4 "schemaVersion": 1, 5 "description": "Energoreport â data z Äeské energetiky: burzovnà ceny elektÅiny a plynu (PXE, TTF), emisnà povolenky, ropa, spotové ceny OTE, výroba a pÅeshraniÄnà toky (ENTSO-E), zásobnÃky plynu CZ/EU, ceny a dodávky tepla, podÃl OZE a koneÄná spotÅeba energie vÄ. výhledů. Data se obnovujà automaticky každý den v 5:30.", 6 "mcpTools": [ 7 "mcp__workspace__web_fetch" 8 ], 9 "mcpServerNames": [ 10 "workspace" 11 ] 12} 13</script>
13 14<html lang="cs"> 15<head> 16<meta charset="utf-8" /> 17<meta name="viewport" content="width=device-width,initial-scale=1" /> 18<meta name="robots" content="noindex, nofollow, noarchive, nosnippet" /> 19<meta name="googlebot" content="noindex, nofollow" /> 20<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate" /> 21<meta http-equiv="pragma" content="no-cache" /> 22<meta http-equiv="expires" content="0" /> 23<title>Energoreport</title> 24<!-- Safari nepodporuje data-URL/SVG favicony â skuteÄné soubory nasazované vedle index.html --> 25<link rel="icon" type="image/png" sizes="64x64" href="favicon.png"> 26<link rel="icon" href="favicon.ico" sizes="32x32"> 27<link rel="apple-touch-icon" href="apple-touch-icon.png">
28<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
vendor: 1 bytes, line 28
28
29<script> 30window.__SUPABASE_URL = 'https://kgjqffzudemmnsjttgxu.supabase.co'; 31window.__SUPABASE_ANON = 'sb_publishable_NmSCvKfxREJ0HqD89o1-QQ_3zQ9k58l'; 32window.__SUPA = null; // Lazy-init po naÄtenà supabase-js 33</script>
vendor: 1 bytes, line 33
33
34<script> 35/* ECB EUR->CZK denni referencni kurz, forward-fill od 2007-01-01. Prepocet EUR/MWh -> CZK/MWh. */ 36window.FX_BASE_MS = Date.UTC(2007,0,1); 37window.FX_DAILY = [27.485,27.525,27.455,27.605,27.635,27.635,27.635,27.649,27.6,27.735,27.774,27.755,27.755,27.755,27.765,27.763,27.868,27.855,27.768,27.768,27.768,27.805,27.914,28.063,28.148,28.125,28.125,28.125,28.215,28.3,28.163,28.095,28.202,28.202,28.202,28.138,28.125,28.128,28.212,28.245,28.245,28.245,28.31,28.233,28.268,28.28,28.249,28.249,28.249,28.16,28.125,28.167,28.237,28.325,28.325,28.325,28.404,28.463,28.295,28.225,28.158,28.158,28.158,28.188,28.176,28.153,28.145,28.168,28.168,28.168,28.238,28.202,28.148,28.056,27.868,27.868,27.868,27.778,27.808,27.938,27.986,27.973,27.973,27.973,27.935,27.986,28.075,28.035,28.01,28.01,28.01,27.94,28.011,27.933,27.943,27.943,27.943,27.943,27.943,27.95,28.003,27.963,27.965,27.965,27.965,27.963,27.964,27.992,28.02,28.033,28.033,28.033,28.026,28.065,28.106,28.115,28.166,28.166,28.166,28.123,28.123,28.127,28.137,28.118,28.118,28.118,28.167,28.232,28.236,28.257,28.272,28.272,28.272,28.288,28.273,28.235,28.11,28.2,28.2,28.2,28.164,28.186,28.247,28.252,28.308,28.308,28.308,28.325,28.288,28.34,28.324,28.285,28.285,28.285,28.323,28.399,28.433,28.412,28.42,28.42,28.42,28.442,28.462,28.511,28.603,28.586,28.586,28.586,28.599,28.597,28.728,28.605,28.65,28.65,28.65,28.718,28.696,28.648,28.633,28.718,28.718,28.718,28.737,28.777,28.699,28.65,28.624,28.624,28.624,28.675,28.629,28.475,28.353,28.318,28.318,28.318,28.205,28.271,28.248,28.281,28.253,28.253,28.253,28.192,28.183,28.121,28.124,28.027,28.027,28.027,28.025,28.037,28.0,28.017,28.043,28.043,28.043,28.067,28.149,28.183,28.119,28.044,28.044,28.044,28.046,27.978,27.935,27.51,27.663,27.663,27.663,27.699,27.668,27.749,27.734,27.693,27.693,27.693,27.808,27.716,27.628,27.594,27.726,27.726,27.726,27.67,27.645,27.64,27.608,27.631,27.631,27.631,27.6,27.63,27.515,27.483,27.508,27.508,27.508,27.501,27.499,27.673,27.487,27.523,27.523,27.523,27.568,27.554,27.59,27.606,27.532,27.532,27.532,27.538,27.495,27.592,27.535,27.53,27.53,27.53,27.513,27.483,27.49,27.473,27.482,27.482,27.482,27.523,27.526,27.44,27.447,27.239,27.239,27.239,27.215,27.182,27.178,27.114,26.962,26.962,26.962,26.922,26.86,26.973,27.026,26.941,26.941,26.941,26.975,26.95,26.934,26.894,26.73,26.73,26.73,26.645,26.691,26.695,26.624,26.655,26.655,26.655,26.693,26.69,26.781,26.765,26.759,26.759,26.759,26.765,26.767,26.494,26.38,26.261,26.261,26.261,26.242,26.29,26.237,26.121,26.164,26.164,26.164,25.998,26.04,26.04,26.248,26.415,26.415,26.415,26.395,26.353,26.3,26.372,26.448,26.448,26.448,26.545,26.545,26.545,26.608,26.58,26.58,26.58,26.628,26.628,26.364,26.175,26.129,26.129,26.129,26.145,26.125,25.98,25.856,25.908,25.908,25.908,25.865,25.905,26.051,26.128,26.118,26.118,26.118,26.319,26.2,26.07,25.974,25.912,25.912,25.912,25.89,25.903,26.006,26.07,25.858,25.858,25.858,25.751,25.671,25.635,25.621,25.658,25.658,25.658,25.65,25.605,25.48,25.346,25.224,25.224,25.224,25.234,25.319,25.29,25.088,25.046,25.046,25.046,25.0,25.02,25.048,25.143,25.228,25.228,25.228,25.068,24.916,25.048,25.125,25.17,25.17,25.17,25.041,25.125,25.066,25.12,25.034,25.034,25.034,25.017,25.25,25.463,25.491,25.491,25.491,25.491,25.491,25.455,25.595,25.383,25.25,25.25,25.25,25.335,25.184,25.07,24.938,25.035,25.035,25.035,25.019,24.985,25.098,25.129,25.025,25.025,25.025,24.94,24.819,24.848,24.975,25.12,25.12,25.12,25.091,25.055,25.075,25.128,25.254,25.254,25.254,25.161,25.25,25.205,25.205,25.265,25.265,25.265,25.227,25.175,25.142,25.171,25.147,25.147,25.147,24.95,24.939,25.029,25.04,24.977,24.977,24.977,25.056,25.062,25.085,25.147,25.103,25.103,25.103,25.083,25.143,25.209,25.06,25.088,25.088,25.088,25.03,24.84,24.67,24.57,24.583,24.583,24.583,24.653,24.433,24.4,24.331,24.195,24.195,24.195,24.211,24.194,24.0,24.11,24.17,24.17,24.17,24.125,24.075,24.07,24.086,24.002,24.002,24.002,23.893,23.825,23.865,23.816,23.694,23.694,23.694,23.55,23.608,23.49,23.465,23.515,23.515,23.515,23.31,23.375,23.213,23.142,23.063,23.063,23.063,22.968,23.013,23.753,23.58,23.595,23.595,23.595,23.695,23.724,23.941,23.947,24.003,24.003,24.003,23.994,23.928,23.98,24.105,24.211,24.211,24.211,24.101,23.954,23.942,24.351,24.485,24.485,24.485,24.523,24.398,24.408,24.385,24.373,24.373,24.373,24.402,24.52,24.533,24.703,24.735,24.735,24.735,24.815,24.848,24.798,24.782,24.822,24.822,24.822,24.938,24.768,24.85,24.63,24.436,24.436,24.436,24.3
3713,24.07,24.005,23.965,24.242,24.242,24.242,24.075,24.143,24.329,24.414,24.436,24.436,24.436,24.605,24.66,24.513,24.764,24.795,24.795,24.795,24.675,24.48,24.568,24.675,24.935,24.935,24.935,24.653,24.611,24.758,24.813,25.232,25.232,25.232,24.986,25.308,25.485,25.827,24.995,24.995,24.995,24.668,24.34,23.88,24.48,24.22,24.22,24.22,24.28,24.163,24.3,24.905,25.092,25.092,25.092,25.273,25.35,25.415,25.293,25.369,25.369,25.369,25.395,25.655,25.718,25.635,25.7,25.7,25.7,25.383,25.458,25.08,25.18,25.209,25.209,25.209,25.613,25.688,25.636,25.72,25.765,25.765,25.765,25.712,25.738,25.9,25.965,25.99,25.99,25.99,26.157,26.371,26.255,26.562,26.335,26.335,26.335,26.44,26.314,26.394,26.394,26.394,26.394,26.394,26.455,26.63,26.875,26.875,26.825,26.825,26.825,26.76,26.41,26.117,26.198,26.46,26.46,26.46,26.57,26.765,26.917,27.308,27.17,27.17,27.17,27.708,27.895,27.584,27.7,28.106,28.106,28.106,27.712,27.638,27.351,27.48,27.882,27.882,27.882,28.132,28.398,28.38,28.248,28.02,28.02,28.02,27.799,28.02,28.575,28.62,28.56,28.56,28.56,29.15,29.49,28.847,28.587,28.81,28.81,28.81,28.451,28.363,28.35,28.333,28.09,28.09,28.09,28.291,27.961,27.736,27.744,28.038,28.038,28.038,27.64,27.213,26.963,27.015,26.601,26.601,26.601,26.523,26.513,26.988,26.779,26.628,26.628,26.628,26.82,27.015,27.29,27.233,27.225,27.225,27.225,27.469,27.388,27.151,26.888,26.593,26.593,26.593,26.558,26.569,26.593,26.51,26.51,26.51,26.51,26.51,26.571,26.883,26.92,26.797,26.797,26.797,27.018,27.035,27.073,26.831,26.736,26.736,26.736,26.636,26.723,26.69,26.701,26.701,26.701,26.701,26.637,26.478,26.805,26.463,26.65,26.65,26.65,26.767,26.744,26.785,26.94,27.028,27.028,27.028,26.92,26.669,26.615,26.727,26.708,26.708,26.708,26.683,26.699,26.725,26.761,26.825,26.825,26.825,26.783,26.83,26.816,26.931,27.003,27.003,27.003,27.0,26.796,26.748,26.745,26.655,26.655,26.655,26.839,26.773,26.695,26.645,26.357,26.357,26.357,26.019,26.207,26.16,26.08,25.998,25.998,25.998,26.02,25.882,25.77,25.755,25.865,25.865,25.865,25.954,25.882,26.047,25.933,26.03,26.03,26.03,26.028,26.014,25.9,25.861,25.918,25.918,25.918,25.838,25.815,25.825,25.625,25.472,25.472,25.472,25.517,25.501,25.54,25.581,25.573,25.573,25.573,25.695,25.863,25.945,25.944,25.91,25.91,25.91,25.678,25.77,25.768,25.738,25.733,25.733,25.733,25.785,25.568,25.711,25.585,25.482,25.482,25.482,25.44,25.356,25.4,25.406,25.42,25.42,25.42,25.376,25.567,25.686,25.615,25.561,25.561,25.561,25.488,25.479,25.522,25.499,25.488,25.488,25.488,25.455,25.351,25.245,25.094,25.089,25.089,25.089,25.167,25.131,25.244,25.159,25.18,25.18,25.18,25.252,25.18,25.164,25.42,25.454,25.454,25.454,25.462,25.539,25.68,25.763,25.866,25.866,25.866,25.821,25.923,25.87,25.872,25.771,25.771,25.771,25.785,25.718,25.879,25.917,25.909,25.909,25.909,25.895,26.084,26.365,26.495,26.458,26.458,26.458,26.462,26.279,26.085,25.87,25.676,25.676,25.676,25.578,25.524,25.379,25.505,25.535,25.535,25.535,25.51,25.495,25.443,25.605,25.887,25.887,25.887,25.844,25.894,25.972,26.188,26.186,26.186,26.186,26.135,25.95,25.972,25.81,25.842,25.842,25.842,25.718,25.757,25.749,25.705,25.727,25.727,25.727,25.734,26.105,26.35,26.105,26.285,26.285,26.285,26.361,26.266,26.403,26.424,26.424,26.424,26.424,26.418,26.413,26.4,26.473,26.473,26.473,26.473,26.285,26.231,26.338,26.37,26.311,26.311,26.311,26.192,26.258,26.168,26.045,25.947,25.947,25.947,25.889,25.905,25.89,25.979,26.183,26.183,26.183,26.011,26.097,26.113,26.232,26.223,26.223,26.223,26.065,25.998,26.024,26.12,26.185,26.185,26.185,26.168,26.099,26.108,26.008,26.025,26.025,26.025,26.002,26.028,25.929,25.72,25.765,25.765,25.765,25.75,25.805,25.88,25.935,25.97,25.97,25.97,25.933,25.77,25.765,25.818,25.771,25.771,25.771,25.578,25.666,25.616,25.555,25.517,25.517,25.517,25.49,25.504,25.402,25.284,25.365,25.365,25.365,25.465,25.442,25.378,25.363,25.418,25.418,25.418,25.453,25.443,25.44,25.385,25.385,25.385,25.385,25.385,25.295,25.24,25.19,25.232,25.232,25.232,25.165,25.155,25.048,25.085,25.178,25.178,25.178,25.25,25.309,25.286,25.365,25.405,25.405,25.405,25.43,25.514,25.57,25.54,25.527,25.527,25.527,25.57,25.71,25.878,25.968,25.976,25.976,25.976,25.603,25.59,25.39,25.416,25.465,25.465,25.465,25.56,25.458,25.727,25.913,25.917,25.917,25.917,25.664,25.647,25.55,25.629,25.78,25.78,25.78,25.505,25.628,25.779,25.75,26.055,26.055,26.055,25.898,25.92,25.892,25.965,25.695,25.695,25.695,25.681,25.663,25.741,25.731,25.733,25.733,25.733,25.76,25.788,25.725,25.773,25.795,25.795,25.795,25.745,25.753,25.691,25.77,25.753,25.753,25.753,25.586,25.485,25.548,25.448,25.36,25.36,25.36,25.325,25.344,25.47,25.464,25.428,25.428,25.428,25.429,25.35,25.346,25.179,25.18,25.18,25.18,25.125,25.09,24.985,24.763,24.777,24.777,24.777,24.68,24.714,24.735,24.75,24.755,24.755,24.755,24.768,24.775,24.82,24.89,24.885,24.885,24.885,24.853,24.805,24.816,24.793,24.803,24.803,24.803,24.805,24.888,24.92,24.858,24.755,24.755,24.755,24.808,24.85,24.735,24.71,24.698,24.698,24.698,24.703,24.726,24.718,24.683,24.681,24.681,24.681,24.672,24.563,24.615,24.616,24.68,24.68,24.68,24.675,24.655,24.578,24.624,24.633,24.633,24.633,24.57,24.618,24.57,24.6,24.42,24.42,24.42,24.469,24.498,24.53,24.518,24.49,24.49,24.49,24.542,24.524,24.47,24.44,24.515,24.515,24.515,24.536,24.52,24.503,24.54,24.626,24.626,24.626,24.51,24.618,24.663,24.63,24.598,24.598,24.598,24.533,24.5,24.497,24.408,24.585,24.585,24.585,24.575,24.583,24.618,24.634,24.63,24.63,24.63,24.627,24.607,24.595,24.648,24.719,24.719,24.719,24.685,24.68,24.688,24.72,24.725,24.725,24.725,24.758,24.915,24.961,25.01,25.018,25.018,25.018,25.043,25.069,25.09,25.073,25.173,25.173,25.173,25.168,25.162,25.155,25.155,25.218,25.218,25.218,25.265,25.258,25.28,25.305,25.328,25.328,25.328,25.35,25.355,25.263,25.24,25.061,25.061,25.061,25.088,24.888,24.875,24.71,24.565,24.565,24.565,24.648,24.553,24.39,24.37,24.384,24.384,24.384,24.335,24.285,24.258,24.42,24.285,24.285,24.285,24.219,24.222,24.219,24.24,24.248,24.248,24.248,24.223,24.111,24.124,24.088,24.018,24.018,24.018,24.07,24.018,24.212,24.246,24.25,24.25,24.25,24.235,24.293,24.325,24.325,24.375,24.375,24.375,24.458,24.495,24.527,24.529,24.479,24.479,24.479,24.353,24.355,24.289,24.208,24.312,24.312,24.312,24.226,24.233,24.29,24.362,24.335,24.335,24.335,24.331,24.394,24.368,24.396,24.388,24.388,24.388,24.473,24.45,24.417,24.558,24.529,24.529,24.529,24.543,24.511,24.528,24.543,24.512,24.512,24.512,24.466,24.443,24.426,24.435,24.43,24.43,24.43,24.435,24.448,24.39,24.269,24.213,24.213,24.213,24.188,24.125,24.174,24.18,24.18,24.18,24.18,24.18,24.1,24.136,24.124,24.223,24.223,24.223,24.175,24.172,24.209,24.192,24.093,24.093,24.093,24.176,24.227,24.258,24.277,24.388,24.388,24.388,24.373,24.454,24.505,24.468,24.476,24.476,24.476,24.53,24.58,24.577,24.618,24.585,24.585,24.585,24.509,24.547,24.501,24.533,24.45,24.45,24.45,24.341,24.219,24.206,24.138,24.133,24.133,24.133,24.132,24.118,24.215,24.286,24.13,24.13,24.13,24.125,24.213,24.29,24.35,24.378,24.378,24.378,24.442,24.398,24.342,24.345,24.31,24.3
371,24.31,24.278,24.32,24.265,24.28,24.224,24.224,24.224,24.18,24.248,24.366,24.435,24.49,24.49,24.49,24.405,24.48,24.498,24.411,24.408,24.408,24.408,24.388,24.353,24.285,24.215,24.188,24.188,24.188,24.16,24.231,24.291,24.308,24.248,24.248,24.248,24.196,24.22,24.088,24.185,24.186,24.186,24.186,24.316,24.384,24.43,24.423,24.475,24.475,24.475,24.493,24.417,24.488,24.24,24.166,24.166,24.166,24.123,24.098,24.11,24.154,24.304,24.304,24.304,24.462,24.45,24.456,24.405,24.43,24.43,24.43,24.522,24.551,24.545,24.535,24.457,24.457,24.457,24.63,24.653,24.93,24.878,24.87,24.87,24.87,24.675,24.478,24.523,24.563,24.754,24.754,24.754,24.878,24.908,24.811,24.845,24.784,24.784,24.784,24.78,24.779,24.779,24.746,24.74,24.74,24.74,24.762,24.925,24.873,24.903,24.993,24.993,24.993,24.982,24.906,24.927,24.833,24.7,24.7,24.7,24.801,25.03,25.145,24.911,24.992,24.992,24.992,24.996,25.172,25.452,25.496,25.701,25.701,25.701,25.743,25.768,25.606,25.646,25.475,25.475,25.475,25.598,25.485,25.642,25.693,26.031,26.031,26.031,25.757,25.548,25.321,25.279,25.202,25.202,25.202,25.146,25.23,25.328,25.23,25.475,25.475,25.475,25.578,25.633,25.649,25.534,25.343,25.343,25.343,25.292,25.491,25.618,25.625,25.839,25.839,25.839,25.839,25.785,25.82,25.91,25.787,25.787,25.787,25.505,25.688,25.761,25.914,25.851,25.851,25.851,25.818,25.785,25.821,25.581,25.45,25.45,25.45,25.595,25.65,25.545,25.298,25.466,25.466,25.466,25.349,25.422,25.371,25.193,25.156,25.156,25.156,25.27,25.188,25.157,25.151,25.064,25.064,25.064,24.98,25.0,24.812,24.985,25.245,25.245,25.245,25.045,25.097,25.17,25.268,25.001,25.001,25.001,24.907,24.912,25.172,25.075,25.033,25.033,25.033,25.068,24.9,24.843,24.89,24.715,24.715,24.715,24.78,24.866,24.871,24.767,24.715,24.715,24.715,24.563,24.585,24.615,24.558,24.511,24.511,24.511,24.533,24.464,24.629,24.736,24.725,24.725,24.725,24.63,24.603,24.603,24.778,24.73,24.73,24.73,24.773,24.621,24.595,24.704,24.704,24.704,24.704,24.704,24.795,24.823,24.803,24.75,24.75,24.75,24.789,24.799,24.816,24.842,24.919,24.919,24.919,25.043,24.997,24.804,24.758,24.87,24.87,24.87,24.867,24.867,24.903,24.933,25.023,25.023,25.023,25.025,25.133,25.243,25.162,25.253,25.253,25.253,25.395,25.515,25.46,25.515,25.333,25.333,25.333,25.225,25.294,25.495,25.39,25.423,25.423,25.423,25.309,25.512,25.658,25.693,25.787,25.787,25.787,25.733,25.72,25.565,25.325,25.481,25.481,25.481,25.395,25.682,25.578,25.54,25.592,25.592,25.592,25.493,25.495,25.453,25.683,25.775,25.775,25.775,25.813,25.963,25.916,25.81,25.64,25.64,25.64,25.515,25.555,25.5,25.457,25.7,25.7,25.7,25.536,25.431,25.395,25.444,25.395,25.395,25.395,25.388,25.34,25.278,25.331,25.57,25.57,25.57,25.579,25.539,25.56,25.505,25.31,25.31,25.31,25.258,25.255,25.358,25.26,25.285,25.285,25.285,25.158,25.114,25.173,25.135,25.205,25.205,25.205,25.145,25.04,24.962,24.912,25.02,25.02,25.02,24.905,24.783,24.968,24.903,24.906,24.906,24.906,24.845,24.828,24.81,24.917,24.84,24.84,24.84,24.894,24.88,24.794,24.703,24.587,24.587,24.587,24.53,24.548,24.44,24.482,24.433,24.433,24.433,24.505,24.815,24.87,24.904,24.803,24.803,24.803,24.94,24.921,24.983,24.859,25.141,25.141,25.141,25.08,25.06,25.033,24.957,24.915,24.915,24.915,24.9,24.936,24.95,24.94,24.95,24.95,24.95,24.931,24.875,24.794,24.756,24.823,24.823,24.823,24.899,24.908,24.973,24.901,24.9,24.9,24.9,25.021,25.022,25.064,25.226,25.232,25.232,25.232,25.234,25.305,25.395,25.423,25.396,25.396,25.396,25.366,25.445,25.5,25.566,25.534,25.534,25.534,25.418,25.399,25.484,25.422,25.339,25.339,25.339,25.293,25.304,25.265,25.226,25.262,25.262,25.262,25.256,25.243,25.218,25.195,25.208,25.208,25.208,25.245,25.29,25.265,25.289,25.231,25.231,25.231,25.222,25.2,25.254,25.228,25.189,25.189,25.189,25.127,25.127,25.127,25.115,25.14,25.14,25.14,25.151,25.151,25.218,25.26,25.355,25.355,25.355,25.539,25.587,25.529,25.629,25.613,25.613,25.613,25.613,25.613,25.58,25.546,25.635,25.635,25.635,25.626,25.613,25.597,25.597,25.605,25.605,25.605,25.69,25.659,25.652,25.619,25.638,25.638,25.638,25.669,25.647,25.732,25.275,25.243,25.243,25.243,25.24,25.315,25.423,25.383,25.386,25.386,25.386,25.388,25.434,25.393,25.496,25.492,25.492,25.492,25.52,25.561,25.637,25.637,25.677,25.677,25.677,25.66,25.629,25.565,25.51,25.443,25.443,25.443,25.542,25.662,25.632,25.615,25.575,25.575,25.575,25.62,25.647,25.67,25.813,25.832,25.832,25.832,25.738,25.806,25.8,25.74,25.74,25.74,25.74,25.74,25.881,25.828,25.817,25.763,25.763,25.763,25.727,25.762,25.865,25.942,25.871,25.871,25.871,25.864,25.868,25.854,25.875,25.857,25.857,25.857,25.93,25.909,25.909,25.899,25.744,25.744,25.744,25.697,25.799,25.799,25.65,25.64,25.64,25.64,25.693,25.747,25.862,25.792,25.808,25.808,25.808,25.844,25.887,26.003,25.98,25.989,25.989,25.989,26.121,26.098,26.069,26.094,25.993,25.993,25.993,25.968,25.889,25.899,25.795,25.711,25.711,25.711,25.753,25.775,25.846,25.778,25.569,25.569,25.569,25.666,25.613,25.66,25.728,25.718,25.718,25.718,25.724,25.68,25.686,25.806,25.825,25.825,25.825,25.865,25.788,25.858,25.89,25.949,25.949,25.949,25.98,25.983,26.02,26.05,25.875,25.875,25.875,25.938,25.875,25.929,25.898,25.951,25.951,25.951,26.01,25.96,25.968,25.925,25.931,25.931,25.931,25.976,25.977,25.93,25.959,25.948,25.948,25.948,25.914,25.856,25.857,25.955,25.959,25.959,25.959,25.937,25.92,25.978,25.816,25.925,25.925,25.925,25.88,25.874,25.822,25.815,25.8,25.8,25.8,25.855,25.773,25.775,25.738,25.665,25.665,25.665,25.626,25.758,25.713,25.678,25.735,25.735,25.735,25.683,25.725,25.787,25.737,25.765,25.765,25.765,25.833,25.83,25.783,25.82,25.775,25.775,25.775,25.769,25.718,25.813,25.735,25.828,25.828,25.828,25.925,25.933,25.88,25.809,25.69,25.69,25.69,25.73,25.647,25.606,25.558,25.554,25.554,25.554,25.513,25.518,25.598,25.529,25.534,25.534,25.534,25.56,25.615,25.68,25.704,25.782,25.782,25.782,25.803,25.727,25.817,25.773,25.75,25.75,25.75,25.732,25.762,25.742,25.729,25.854,25.854,25.854,25.835,25.833,25.784,26.85,26.9
3766,26.966,26.966,26.996,27.01,27.055,27.176,27.155,27.155,27.155,27.117,27.368,27.329,27.197,27.258,27.258,27.258,27.269,27.329,27.341,27.35,27.391,27.391,27.391,27.407,27.468,27.458,27.45,27.48,27.48,27.48,27.498,27.453,27.434,27.475,27.534,27.534,27.534,27.6,27.657,27.734,27.657,27.66,27.66,27.66,27.574,27.536,27.536,27.536,27.445,27.445,27.445,27.48,27.427,27.427,27.481,27.525,27.525,27.525,27.485,27.454,27.448,27.433,27.392,27.392,27.392,27.41,27.409,27.441,27.491,27.458,27.458,27.458,27.524,27.555,27.535,27.49,27.525,27.525,27.525,27.475,27.507,27.54,27.594,27.5,27.5,27.5,27.527,27.53,27.535,27.525,27.503,27.503,27.503,27.547,27.53,27.533,27.53,27.436,27.436,27.436,27.396,27.392,27.43,27.382,27.368,27.368,27.368,27.362,27.346,27.325,27.339,27.344,27.344,27.344,27.354,27.369,27.353,27.363,27.333,27.333,27.333,27.348,27.35,27.353,27.355,27.358,27.358,27.358,27.411,27.411,27.46,27.488,27.458,27.458,27.458,27.411,27.395,27.459,27.41,27.423,27.423,27.423,27.442,27.449,27.463,27.432,27.429,27.429,27.429,27.444,27.415,27.406,27.427,27.452,27.452,27.452,27.466,27.459,27.463,27.502,27.502,27.502,27.502,27.502,27.481,27.459,27.443,27.46,27.46,27.46,27.458,27.434,27.454,27.454,27.444,27.444,27.444,27.436,27.438,27.412,27.403,27.392,27.392,27.392,27.396,27.404,27.43,27.44,27.443,27.443,27.443,27.474,27.479,27.467,27.443,27.438,27.438,27.438,27.421,27.433,27.442,27.474,27.471,27.471,27.471,27.482,27.463,27.46,27.45,27.461,27.461,27.461,27.451,27.451,27.445,27.42,27.44,27.44,27.44,27.438,27.445,27.481,27.435,27.433,27.433,27.433,27.458,27.443,27.435,27.447,27.459,27.459,27.459,27.453,27.433,27.432,27.439,27.444,27.444,27.444,27.434,27.437,27.433,27.44,27.44,27.44,27.44,27.439,27.431,27.446,27.44,27.433,27.433,27.433,27.468,27.485,27.454,27.48,27.482,27.482,27.482,27.481,27.49,27.509,27.57,27.638,27.638,27.638,27.645,27.668,27.834,27.853,27.825,27.825,27.825,27.852,27.845,27.839,27.845,27.882,27.882,27.882,27.9,28.004,27.928,27.814,27.838,27.838,27.838,27.833,27.841,27.75,27.775,27.725,27.725,27.725,27.738,27.784,27.658,27.662,27.596,27.596,27.596,27.637,27.678,27.724,27.635,27.6,27.6,27.6,27.579,27.555,27.532,27.518,27.574,27.574,27.574,27.543,27.517,27.497,27.572,27.534,27.534,27.534,27.54,27.5,27.502,27.495,27.479,27.479,27.479,27.481,27.47,27.48,27.47,27.493,27.493,27.493,27.545,27.545,27.545,27.546,27.492,27.492,27.492,27.562,27.613,27.683,27.69,27.695,27.695,27.695,27.705,27.749,27.784,27.729,27.77,27.77,27.77,27.788,27.788,27.823,27.769,27.705,27.705,27.705,27.585,27.592,27.603,27.644,27.653,27.653,27.653,27.701,27.677,27.683,27.657,27.66,27.66,27.66,27.592,27.583,27.579,27.609,27.652,27.652,27.652,27.626,27.605,27.623,27.616,27.635,27.635,27.635,27.611,27.618,27.618,27.633,27.59,27.59,27.59,27.565,27.607,27.626,27.606,27.636,27.636,27.636,27.618,27.66,27.777,27.777,27.777,27.777,27.777,27.717,27.728,27.735,27.735,27.693,27.693,27.693,27.676,27.695,27.783,27.865,28.062,28.062,28.062,28.287,28.405,28.229,27.841,27.795,27.795,27.795,27.881,27.849,27.939,27.9,27.831,27.831,27.831,27.754,27.872,27.846,27.792,27.797,27.797,27.797,27.749,27.748,27.77,27.708,27.698,27.698,27.698,27.72,27.727,27.68,27.702,27.64,27.64,27.64,27.638,27.619,27.583,27.373,27.508,27.508,27.508,27.444,27.487,27.419,27.515,27.438,27.438,27.438,27.488,27.453,27.464,27.422,27.297,27.297,27.297,27.251,27.285,27.293,27.303,27.315,27.315,27.315,27.308,27.183,27.318,27.425,27.479,27.479,27.479,27.374,27.39,27.378,27.376,27.523,27.523,27.523,27.473,27.533,27.543,27.57,27.57,27.57,27.57,27.57,27.455,27.378,27.378,27.383,27.383,27.383,27.392,27.345,27.415,27.503,27.486,27.486,27.486,27.417,27.418,27.469,27.399,27.454,27.454,27.454,27.441,27.471,27.435,27.43,27.43,27.43,27.43,27.364,27.362,27.415,27.405,27.411,27.411,27.411,27.4,27.394,27.45,27.458,27.401,27.401,27.401,27.401,27.357,27.33,27.351,27.395,27.395,27.395,27.372,27.407,27.404,27.463,27.401,27.401,27.401,27.44,27.439,27.46,27.467,27.396,27.396,27.396,27.431,27.356,27.318,27.318,27.3,27.3,27.3,27.279,27.254,27.24,27.275,27.211,27.211,27.211,27.206,27.189,27.206,27.235,27.23,27.23,27.23,27.246,27.253,27.246,27.265,27.145,27.145,27.145,27.135,27.1,27.116,27.11,27.123,27.123,27.123,27.102,27.08,27.078,27.116,27.054,27.054,27.054,27.072,27.072,27.068,27.039,27.042,27.042,27.042,27.029,27.023,27.066,27.053,27.031,27.031,27.031,27.041,27.055,27.037,27.029,27.024,27.024,27.024,27.034,27.026,27.021,27.024,27.028,27.028,27.028,27.021,27.021,27.022,27.023,27.021,27.021,27.021,27.068,27.109,27.085,27.098,27.048,27.048,27.048,27.021,27.022,27.023,27.022,27.03,27.03,27.03,27.026,27.047,27.057,27.038,27.074,27.074,27.074,27.116,27.088,27.065,27.112,27.071,27.071,27.071,27.053,27.057,27.09,27.162,27.193,27.193,27.193,27.22,27.208,27.187,27.173,27.163,27.163,27.163,27.122,27.109,27.099,27.117,27.109,27.109,27.109,27.108,27.131,27.113,27.084,27.081,27.081,27.081,27.083,27.08,27.079,27.069,27.074,27.074,27.074,27.088,27.098,27.134,27.113,27.09,27.09,27.09,27.097,27.1,27.074,27.044,27.026,27.026,27.026,27.042,27.044,27.022,27.031,27.032,27.032,27.032,27.031,27.031,27.029,27.025,27.031,27.031,27.031,27.025,27.033,27.025,27.032,27.024,27.024,27.024,27.03,27.023,27.024,27.036,27.035,27.035,27.035,27.022,27.022,27.021,27.021,27.023,27.023,27.023,27.024,27.022,27.03,27.031,27.03,27.03,27.03,27.028,27.028,27.033,27.028,27.028,27.028,27.028,27.022,27.028,27.029,27.023,27.023,27.023,27.023,27.023,27.022,27.026,27.03,27.022,27.022,27.022,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.037,27.023,27.042,27.064,27.026,27.026,27.026,27.021,27.021,27.025,27.021,27.026,27.026,27.026,27.021,27.026,27.025,27.021,27.037,27.037,27.037,27.062,27.058,27.031,27.069,27.063,27.063,27.063,27.037,27.029,27.044,27.03,27.023,27.023,27.023,27.022,27.028,27.047,27.052,27.064,27.064,27.064,27.057,27.056,27.046,27.057,27.058,27.058,27.058,27.056,27.058,27.047,27.037,27.059,27.059,27.059,27.05,27.043,27.05,27.044,27.035,27.035,27.035,27.035,27.037,27.038,27.064,27.064,27.064,27.064,27.064,27.073,27.075,27.051,27.03,27.03,27.03,27.046,27.036,27.029,27.025,27.022,27.022,27.022,27.026,27.03,27.036,27.028,27.025,27.025,27.025,27.022,27.021,27.021,27.024,27.046,27.046,27.046,27.047,27.027,27.034,27.045,27.038,27.038,27.038,27.048,27.037,27.031,27.028,27.021,27.021,27.021,27.022,27.023,27.022,27.023,27.021,27.021,27.021,27.023,27.022,27.022,27.023,27.025,27.025,27.025,27.033,27.025,27.028,27.023,27.028,27.028,27.028,27.021,27.022,27.027,27.029,27.025,27.025,27.025,27.023,27.021,27.021,27.021,27.026,27.026,27.026,27.035,27.048,27.073,27.067,27.069,27.069,27.069,27.063,27.064,27.071,27.062,27.103,27.103,27.103,27.151,27.108,27.114,27.131,27.093,27.093,27.093,27.095,27.071,27.078,27.049,27.033,27.033,27.033,27.037,27.034,27.033,27.049,27.04,27.04,27.04,27.024,27.014,27.022,27.025,27.023,27.023,27.023,27.025,27.021,27.035,27.045,27.031,27.031,27.031,27.029,27.032,27.034,27.027,27.022,27.022,27.022,27.027,27.027,27.025,27.02,27.018,27.018,27.018,27.023,27.02,27.023,27.024,27.022,27.022,27.022,27.024,27.023,27.022,27.026,27.024,27.024,27.024,27.023,27.025,27.026,27.025,27.022,27.022,27.022,27.021,27.021,27.021,27.021,27.022,27.022,27.022,27.024,27.021,27.023,27.023,27.024,27.024,27.024,27.021,27.022,27.022,27.021,27.022,27.022,27.022,27.021,27.025,27.023,27.022,27.021,27.021,27.021,27.021,27.021,27.028,27.021,27.021,27.021,27.021,27.021,27.021,27.025,27.021,27.029,27.029,27.029,27.024,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.022,27.022,27.022,27.022,27.024,27.024,27.022,27.021,27.021,27.021,27.021,27.021,27.021,27.027,27.022,27.054,27.054,27.054,27.026,27.03,27.036,27.05,27.035,27.035,27.035,27.032,27.044,27.027,27.034,27.039,27.039,27.039,27.044,27.045,27.06,27.061,27.058,27.058,27.058,27.057,27.045,27.036,27.038,27.027,27.027,27.027,27.027,27.022,27.023,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.023,27.023,27.023,27.023,27.021,27.042,27.021,27.021,27.021,27.021,27.021,27.02,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.027,27.021,27.022,27.021,27.021,27.021,27.021,27.022,27.021,27.021,27.022,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.022,27.03,27.03,27.03,27.044,27.054,27.058,26.736,26.563,26.563,26.563,26.528,26.658,26.697,26.704,26.704,26.704,26.704,26.704,26.759,26.794,26.907,26.933,26.933,26.933,26.793,26.771,26.947,26.937,26.922,26.922,26.922,26.922,26.893,26.871,26.767,26.781,26.781,26.781,26.712,26.672,26.642,26.598,26.576,26.576,26.576,26.471,26.42,26.435,26.632,26.503,26.503,26.503,26.488,26.483,26.463,26.413,26.434,26.434,26.434,26.435,26.467,26.422,26.397,26.36,26.36,26.36,26.327,26.336,26.316,26.268,26.223,26.223,26.223,26.198,26.147,26.163,26.213,26.233,26.233,26.233,26.172,26.293,26.266,26.27,26.297,26.297,26.297,26.243,26.27,26.326,26.3,26.197,26.197,26.197,26.14,26.132,26.109,26.147,26.079,26.079,26.079,26.096,26.124,26.11,26.12,26.075,26.075,26.075,26.09,26.08,26.035,26.04,26.017,26.017,26.017,26.024,26.013,26.047,26.045,26.048,26.048,26.048,26.079,26.132,26.128,25.965,26.068,26.068,26.068,26.116,26.15,26.16,26.157,26.155,26.155,26.155,26.118,26.142,26.044,26.012,26.108,26.108,26.108,26.086,26.083,26.124,26.098,26.084,26.084,26.084,26.102,26.142,26.045,26.101,26.077,26.077,26.077,26.062,26.077,26.112,26.113,26.099,26.099,26.099,26.077,26.105,26.101,26.103,26.081,26.081,26.081,26.091,26.103,26.091,26.083,26.046,26.046,26.046,26.033,26.056,26.045,26.041,25.981,25.981,25.981,25.995,25.932,25.886,25.837,25.816,25.816,25.816,25.893,25.9,25.879,25.891,25.813,25.813,25.813,25.77,25.733,25.705,25.725,25.69,25.69,25.69,25.641,25.583,25.589,25.589,25.673,25.673,25.673,25.645,25.669,25.557,25.535,25.652,25.
37652,25.652,25.612,25.591,25.56,25.528,25.543,25.543,25.543,25.567,25.566,25.669,25.559,25.588,25.588,25.588,25.568,25.533,25.469,25.462,25.413,25.413,25.413,25.431,25.463,25.474,25.491,25.524,25.524,25.524,25.557,25.653,25.636,25.611,25.555,25.555,25.555,25.601,25.596,25.65,25.695,25.678,25.678,25.678,25.685,25.662,25.674,25.715,25.75,25.75,25.75,25.75,25.75,25.84,25.645,25.535,25.535,25.535,25.535,25.494,25.545,25.515,25.594,25.594,25.594,25.524,25.535,25.57,25.547,25.521,25.521,25.521,25.531,25.516,25.447,25.365,25.433,25.433,25.433,25.399,25.403,25.374,25.381,25.357,25.357,25.357,25.297,25.33,25.272,25.267,25.192,25.192,25.192,25.196,25.233,25.23,25.247,25.335,25.335,25.335,25.358,25.386,25.37,25.37,25.34,25.34,25.34,25.327,25.318,25.361,25.316,25.337,25.337,25.337,25.391,25.398,25.418,25.434,25.409,25.409,25.409,25.404,25.38,25.421,25.427,25.454,25.454,25.454,25.448,25.471,25.447,25.416,25.415,25.415,25.415,25.432,25.423,25.422,25.399,25.405,25.405,25.405,25.446,25.473,25.462,25.425,25.425,25.425,25.425,25.425,25.367,25.34,25.327,25.339,25.339,25.339,25.357,25.335,25.323,25.316,25.307,25.307,25.307,25.265,25.254,25.303,25.327,25.34,25.34,25.34,25.408,25.437,25.466,25.472,25.471,25.471,25.471,25.542,25.542,25.601,25.585,25.503,25.503,25.503,25.518,25.526,25.564,25.498,25.506,25.506,25.506,25.488,25.551,25.545,25.557,25.59,25.59,25.59,25.68,25.702,25.813,25.809,25.765,25.765,25.765,25.731,25.907,25.835,25.797,25.827,25.827,25.827,25.696,25.653,25.648,25.64,25.795,25.795,25.795,25.682,25.652,25.711,25.651,25.714,25.714,25.714,25.739,25.811,25.835,25.863,25.792,25.792,25.792,25.935,25.9,25.777,26.001,26.02,26.02,26.02,25.998,26.073,26.028,25.938,25.942,25.942,25.942,25.858,25.898,25.929,25.938,25.924,25.924,25.924,25.883,25.875,25.853,25.92,25.875,25.875,25.875,25.848,25.761,25.675,25.647,25.622,25.622,25.622,25.624,25.598,25.59,25.589,25.657,25.657,25.657,25.65,25.606,25.578,25.57,25.635,25.635,25.635,25.656,25.692,25.705,25.702,25.729,25.729,25.729,25.706,25.737,25.725,25.713,25.744,25.744,25.744,25.744,25.713,25.745,25.749,25.735,25.735,25.735,25.749,25.728,25.746,25.715,25.697,25.697,25.697,25.648,25.646,25.59,25.52,25.473,25.473,25.473,25.457,25.448,25.434,25.56,25.585,25.585,25.585,25.615,25.613,25.605,25.715,25.731,25.731,25.731,25.765,25.788,25.779,25.78,25.742,25.742,25.742,25.737,25.805,25.807,25.848,25.777,25.777,25.777,25.798,25.821,25.843,25.863,25.867,25.867,25.867,25.848,25.814,25.833,25.831,25.86,25.86,25.86,25.849,25.87,25.922,25.879,25.782,25.782,25.782,25.846,25.833,25.88,25.887,25.936,25.936,25.936,25.942,25.939,25.994,25.985,25.985,25.985,25.985,26.013,26.032,26.002,25.99,25.954,25.954,25.954,25.918,25.914,25.931,25.967,25.957,25.957,25.957,25.921,25.901,25.886,25.89,25.851,25.851,25.851,25.866,25.845,25.866,25.829,25.794,25.794,25.794,25.801,25.753,25.765,25.76,25.855,25.855,25.855,25.916,25.916,25.916,25.858,25.778,25.778,25.778,25.724,25.724,25.752,25.683,25.653,25.653,25.653,25.572,25.642,25.629,25.626,25.606,25.606,25.606,25.561,25.572,25.564,25.537,25.58,25.58,25.58,25.593,25.612,25.693,25.695,25.697,25.697,25.697,25.726,25.747,25.802,25.76,25.695,25.695,25.695,25.728,25.697,25.781,25.809,25.806,25.806,25.806,25.836,25.871,25.795,25.79,25.709,25.709,25.709,25.715,25.717,25.679,25.648,25.667,25.667,25.667,25.653,25.663,25.661,25.601,25.636,25.636,25.636,25.621,25.623,25.592,25.61,25.641,25.641,25.641,25.656,25.667,25.668,25.668,25.668,25.668,25.668,25.616,25.604,25.646,25.648,25.727,25.727,25.727,25.759,25.769,25.797,25.786,25.802,25.802,25.802,25.791,25.753,25.724,25.693,25.
37613,25.613,25.613,25.634,25.618,25.608,25.606,25.623,25.623,25.623,25.625,25.663,25.657,25.682,25.682,25.682,25.682,25.682,25.743,25.729,25.737,25.705,25.705,25.705,25.682,25.659,25.659,25.648,25.712,25.712,25.712,25.715,25.731,25.727,25.718,25.732,25.732,25.732,25.776,25.751,25.76,25.696,25.753,25.753,25.753,25.766,25.775,25.793,25.821,25.83,25.83,25.83,25.834,25.843,25.861,25.841,25.816,25.816,25.816,25.832,25.744,25.657,25.663,25.642,25.642,25.642,25.626,25.64,25.614,25.581,25.54,25.54,25.54,25.56,25.609,25.635,25.619,25.609,25.609,25.609,25.601,25.557,25.486,25.434,25.447,25.447,25.447,25.478,25.462,25.458,25.434,25.471,25.471,25.471,25.509,25.543,25.565,25.605,25.59,25.59,25.59,25.569,25.575,25.603,25.586,25.546,25.546,25.546,25.534,25.549,25.542,25.519,25.542,25.542,25.542,25.613,25.65,25.658,25.742,25.763,25.763,25.763,25.776,25.727,25.719,25.789,25.828,25.828,25.828,25.831,25.829,25.884,25.876,25.734,25.734,25.734,25.78,25.796,25.794,25.784,25.769,25.769,25.769,25.792,25.814,25.844,25.853,25.914,25.914,25.914,25.919,25.885,25.834,25.853,25.836,25.836,25.836,25.855,25.869,25.867,25.824,25.843,25.843,25.843,25.882,25.916,25.892,25.892,25.913,25.913,25.913,25.89,25.872,25.867,25.851,25.842,25.842,25.842,25.816,25.74,25.739,25.739,25.741,25.741,25.741,25.781,25.805,25.823,25.893,25.807,25.807,25.807,25.823,25.82,25.758,25.673,25.659,25.659,25.659,25.63,25.57,25.638,25.598,25.568,25.568,25.568,25.48,25.543,25.512,25.509,25.514,25.514,25.514,25.511,25.536,25.498,25.534,25.486,25.486,25.486,25.51,25.499,25.591,25.56,25.583,25.583,25.583,25.593,25.565,25.541,25.52,25.513,25.513,25.513,25.485,25.505,25.515,25.574,25.515,25.515,25.515,25.534,25.527,25.52,25.526,25.533,25.533,25.533,25.528,25.527,25.522,25.519,25.508,25.508,25.508,25.483,25.445,25.49,25.473,25.445,25.445,25.445,25.499,25.485,25.485,25.485,25.509,25.509,25.509,25.463,25.408,25.408,25.411,25.36,25.36,25.36,25.301,25.276,25.265,25.253,25.265,25.265,25.265,25.23,25.155,25.144,25.17,25.147,25.147,25.147,25.125,25.066,25.136,25.159,25.16,25.16,25.16,25.231,25.221,25.208,25.25,25.21,25.21,25.21,25.177,25.143,25.055,24.893,25.03,25.03,25.03,25.026,24.965,24.878,24.835,24.828,24.828,24.828,24.793,24.898,24.962,25.037,25.061,25.061,25.061,25.186,25.226,25.344,25.283,25.39,25.39,25.39,25.525,25.475,25.34,25.346,25.458,25.458,25.458,25.504,25.725,25.77,26.203,26.042,26.042,26.042,26.96,26.985,27.154,27.606,27.191,27.191,27.191,27.635,27.808,27.446,27.55,27.299,27.299,27.299,27.315,27.312,27.369,27.553,27.539,27.539,27.539,27.603,27.215,27.183,26.909,26.909,26.909,26.909,26.909,26.864,26.991,27.053,27.163,27.163,27.163,27.328,27.447,27.534,27.551,27.31,27.31,27.31,27.184,27.227,27.125,27.097,27.097,27.097,27.097,27.119,26.978,26.987,27.158,27.251,27.251,27.251,27.6,27.423,27.408,27.571,27.589,27.589,27.589,27.61,27.49,27.43,27.212,27.21,27.21,27.21,27.275,27.073,27.051,27.017,26.921,26.921,26.921,26.859,26.645,26.66,26.623,26.589,26.589,26.589,26.585,26.634,26.609,26.679,26.698,26.698,26.698,26.683,26.564,26.561,26.689,26.683,26.683,26.683,26.681,26.612,26.733,26.796,26.808,26.808,26.808,26.848,26.74,26.67,26.632,26.665,26.665,26.665,26.705,26.681,26.748,26.622,26.691,26.691,26.691,26.648,26.638,26.587,26.693,26.682,26.682,26.682,26.598,26.429,26.365,26.342,26.268,26.268,26.268,26.193,26.251,26.291,26.248,26.175,26.175,26.175,26.319,26.221,26.097,26.202,26.279,26.279,26.279,26.183,26.155,26.117,26.125,26.118,26.118,26.118,26.14,26.134,26.118,26.061,26.083,26.083,26.083,26.091,26.121,26.262,26.276,26.2,26.2,26.2,26.208,26.226,26.338,26.368,26.435,26.435,26.435,26.468,26.521,26.519,26.585,26.566,26.566,26.566,26.66,26.827,26.726,26.747,26.727,26.727,26.727,26.98,27.007,26.965,27.015,27.107,27.107,27.107,27.133,27.148,27.233,26.918,27.02,27.02,27.02,27.096,27.035,27.064,27.092,27.11,27.11,27.11,27.177,27.356,27.313,27.34,27.279,27.279,27.279,27.346,27.233,27.185,27.23,27.222,27.222,27.222,27.295,27.337,27.422,27.368,27.251,27.251,27.251,27.131,26.913,26.781,26.766,26.667,26.667,26.667,26.475,26.431,26.447,26.465,26.461,26.461,26.461,26.353,26.508,26.401,26.358,26.34,26.34,26.34,26.311,26.258,26.175,26.138,26.213,26.213,26.213,26.192,26.235,26.411,26.42,26.518,26.518,26.518,26.482,26.398,26.239,26.307,26.328,26.328,26.328,26.317,26.361,26.2,26.204,26.139,26.139,26.139,26.275,26.303,26.372,26.299,26.299,26.299,26.299,26.247,26.302,26.252,26.242,26.242,26.242,26.242,26.141,26.227,26.145,26.147,26.163,26.163,26.163,26.239,26.19,26.173,26.19,26.163,26.163,26.163,26.185,26.161,26.125,26.094,26.152,26.152,26.152,26.08,26.08,26.025,26.114,26.02,26.02,26.02,25.975,25.9,25.925,25.895,25.806,25.806,25.806,25.739,25.738,25.835,25.772,25.753,25.753,25.753,25.682,25.753,25.883,25.864,25.849,25.849,25.849,25.954,25.906,25.978,26.11,26.195,26.195,26.195,26.087,26.142,26.138,26.208,26.303,26.303,26.303,26.417,26.288,26.226,26.154,26.15,26.15,26.15,26.193,26.197,26.159,26.17,26.127,26.127,26.127,26.075,26.198,26.206,26.233,26.081,26.081,26.081,26.079,26.122,26.143,26.085,26.085,26.085,26.085,26.085,26.051,25.9
3719,25.875,25.945,25.945,25.945,26.031,26.016,25.929,25.942,25.927,25.927,25.927,25.903,25.903,25.893,25.86,25.847,25.847,25.847,25.807,25.895,25.96,25.817,25.883,25.883,25.883,25.773,25.841,25.825,25.847,25.682,25.682,25.682,25.591,25.571,25.522,25.575,25.492,25.492,25.492,25.498,25.427,25.445,25.519,25.452,25.452,25.452,25.424,25.447,25.451,25.439,25.451,25.451,25.451,25.454,25.462,25.465,25.448,25.451,25.451,25.451,25.4,25.394,25.377,25.391,25.335,25.335,25.335,25.422,25.45,25.472,25.51,25.519,25.519,25.519,25.569,25.533,25.408,25.423,25.487,25.487,25.487,25.499,25.477,25.488,25.507,25.606,25.606,25.606,25.586,25.61,25.688,25.894,25.791,25.791,25.791,25.723,25.644,25.646,25.587,25.538,25.538,25.538,25.605,25.642,25.695,25.639,25.65,25.65,25.65,25.629,25.685,25.625,25.506,25.501,25.501,25.501,25.476,25.49,25.44,25.415,25.413,25.413,25.413,25.391,25.382,25.405,25.393,25.386,25.386,25.386,25.427,25.441,25.447,25.501,25.559,25.559,25.559,25.514,25.533,25.531,25.546,25.563,25.563,25.563,25.566,25.523,25.406,25.382,25.422,25.422,25.422,25.401,25.423,25.394,25.398,25.288,25.288,25.288,25.348,25.389,25.318,25.281,25.296,25.296,25.296,25.432,25.4,25.38,25.352,25.408,25.408,25.408,25.443,25.517,25.457,25.495,25.309,25.309,25.309,25.32,25.308,25.415,25.415,25.434,25.434,25.434,25.399,25.376,25.369,25.387,25.403,25.403,25.403,25.453,25.505,25.519,25.615,25.655,25.655,25.655,25.734,25.7,25.684,25.709,25.697,25.697,25.697,25.634,25.572,25.546,25.469,25.275,25.275,25.275,25.252,25.197,25.209,25.254,25.238,25.238,25.238,25.217,25.227,25.22,25.23,25.413,25.413,25.413,25.373,25.445,25.501,25.475,25.662,25.662,25.662,25.671,25.526,25.443,25.42,25.415,25.415,25.415,25.421,25.465,25.475,25.434,25.365,25.365,25.365,25.401,25.334,25.271,25.282,25.255,25.255,25.255,25.234,25.24,25.24,25.088,25.014,25.014,25.014,25.139,24.98,24.958,24.917,24.858,24.858,24.858,24.818,24.745,24.581,24.528,24.439,24.439,24.439,24.357,24.412,24.423,24.458,24.493,24.493,24.493,24.467,24.426,24.313,24.263,24.347,24.347,24.347,24.528,24.5,24.531,24.427,24.443,24.443,24.443,24.372,24.335,24.298,24.135,24.36,24.36,24.36,24.222,24.259,24.288,24.35,24.405,24.405,24.405,24.527,24.419,24.365,24.383,24.337,24.337,24.337,24.345,24.496,24.473,25.09,24.66,24.66,24.66,24.997,25.465,25.866,25.634,25.737,25.737,25.737,25.584,25.642,25.364,25.316,25.213,25.213,25.213,24.89,24.867,24.687,24.777,24.837,24.837,24.837,24.683,24.679,24.605,24.72,24.645,24.645,24.645,24.65,24.464,24.45,24.375,24.376,24.376,24.376,24.32,24.338,24.441,24.512,24.479,24.479,24.479,24.429,24.45,24.45,24.42,24.42,24.42,24.42,24.42,24.424,24.409,24.38,24.32,24.32,24.32,24.418,24.423,24.55,24.526,24.605,24.605,24.605,24.671,24.662,24.644,24.606,24.665,24.665,24.665,25.055,25.014,25.365,24.925,24.74,24.74,24.74,24.71,24.712,24.647,24.7,24.67,24.67,24.67,24.594,24.663,24.648,24.676,24.7,24.7,24.7,24.712,24.714,24.748,24.702,24.708,24.708,24.708,24.715,24.739,24.622,24.689,24.705,24.705,24.705,24.724,24.749,24.703,24.742,24.742,24.742,24.742,24.728,24.69,24.712,24.75,24.731,24.731,24.731,24.724,24.726,24.739,24.739,24.753,24.753,24.753,24.745,24.751,24.778,24.779,24.614,24.614,24.614,24.592,24.582,24.397,24.417,24.561,24.561,24.561,24.508,24.555,24.493,24.496,24.514,24.514,24.514,24.535,24.607,24.575,24.609,24.61,24.61,24.61,24.628,24.644,24.65,24.659,24.581,24.581,24.581,24.515,24.532,24.397,24.346,24.38,24.38,24.38,24.46,24.54,24.566,24.611,24.625,24.625,24.625,24.651,24.658,24.629,24.648,24.635,24.635,24.635,24.592,24.577,24.55,24.488,24.481,24.481,24.481,24.622,24.55,24.631,24.543,24.536,24.536,24.536,24.546,24.551,24.527,24.518,24.497,24.497,24.497,24.494,24.556,24.637,24.657,24.658,24.658,24.658,24.64,24.661,24.65,24.687,24.549,24.549,24.549,24.527,24.544,24.524,24.479,24.517,24.517,24.517,24.521,24.535,24.561,24.569,24.587,24.587,24.587,24.562,24.593,24.563,24.525,24.511,24.511,24.511,24.482,24.472,24.535,24.53,24.465,24.465,24.465,24.488,24.484,24.506,24.539,24.422,24.422,24.422,24.301,24.326,24.337,24.361,24.278,24.278,24.278,24.289,24.326,24.355,24.399,24.351,24.351,24.351,24.356,24.351,24.356,24.392,24.367,24.367,24.367,24.348,24.334,24.338,24.361,24.377,24.377,24.377,24.351,24.316,24.322,24.324,24.293,24.293,24.293,24.307,24.287,24.276,24.27,24.262,24.262,24.262,24.233,24.181,24.218,24.215,24.247,24.247,24.247,24.247,24.26,24.252,24.191,24.116,24.116,24.116,24.176,24.124,24.028,24.028,24.054,24.054,24.054,23.99,23.984,24.027,24.036,24.011,24.011,24.011,23.997,23.966,23.954,23.924,23.922,23.922,23.922,23.881,23.874,23.808,23.818,23.826,23.826,23.826,23.861,23.792,23.775,23.809,23.725,23.725,23.725,23.843,23.832,23.782,23.698,23.692,23.692,23.692,23.739,23.749,23.666,23.685,23.715,23.715,23.715,23.693,23.73,23.669,23.686,23.643,23.643,23.643,23.619,23.497,23.426,23.487,23.508,23.508,23.508,23.545,23.495,23.585,23.62
376,23.635,23.635,23.635,23.747,23.791,23.942,24.011,23.948,23.948,23.948,23.988,23.846,23.722,23.658,23.682,23.682,23.682,23.732,23.668,23.631,23.547,23.492,23.492,23.492,23.44,23.418,23.429,23.409,23.409,23.409,23.409,23.409,23.469,23.421,23.271,23.341,23.341,23.341,23.345,23.373,23.475,23.502,23.565,23.565,23.565,23.439,23.476,23.477,23.512,23.502,23.502,23.502,23.502,23.587,23.559,23.46,23.401,23.401,23.401,23.41,23.363,23.419,23.488,23.606,23.606,23.606,23.574,23.675,23.632,23.683,23.764,23.764,23.764,23.676,23.681,23.671,23.645,23.648,23.648,23.648,23.693,23.709,23.741,23.687,23.657,23.657,23.657,23.573,23.52,23.625,23.625,23.666,23.666,23.666,23.759,23.813,23.796,23.751,23.817,23.817,23.817,23.778,23.756,23.763,23.683,23.654,23.654,23.654,23.649,23.609,23.683,23.686,23.742,23.742,23.742,23.711,23.695,23.775,23.839,23.945,23.945,23.945,23.843,23.855,23.828,23.752,23.777,23.777,23.777,23.757,23.818,23.927,23.961,24.062,24.062,24.062,24.139,24.047,24.05,24.032,24.028,24.028,24.028,23.9,23.951,23.929,24.034,24.26,24.26,24.26,24.224,24.24,24.293,24.223,24.103,24.103,24.103,24.038,24.138,24.06,24.078,24.037,24.037,24.037,24.02,24.0,24.132,24.13,24.117,24.117,24.117,24.138,24.163,24.107,24.072,24.118,24.118,24.118,24.108,24.161,24.217,24.37,24.452,24.452,24.452,24.508,24.605,24.487,24.468,24.496,24.496,24.496,24.406,24.432,24.388,24.437,24.36,24.36,24.36,24.377,24.411,24.47,24.378,24.339,24.339,24.339,24.46,24.494,24.428,24.416,24.423,24.423,24.423,24.477,24.566,24.563,24.55,24.67,24.67,24.67,24.664,24.603,24.68,24.674,24.704,24.704,24.704,24.645,24.659,24.693,24.714,24.658,24.658,24.658,24.557,24.558,24.677,24.634,24.414,24.414,24.414,24.443,24.622,24.666,24.555,24.525,24.525,24.525,24.581,24.551,24.506,24.446,24.461,24.461,24.461,24.541,24.509,24.48,24.367,24.406,24.406,24.406,24.364,24.365,24.26,24.292,24.331,24.331,24.331,24.394,24.369,24.335,24.356,24.354,24.354,24.354,24.367,24.42,24.476,24.408,24.477,24.477,24.477,24.543,24.525,24.538,24.481,24.589,24.589,24.589,24.589,24.589,24.673,24.714,24.724,24.724,24.724,24.724,24.687,24.675,24.652,24.616,24.616,24.616,24.488,24.594,24.562,24.659,24.689,24.689,24.689,24.714,24.71,24.755,24.734,24.813,24.813,24.813,24.758,24.824,24.786,24.756,24.748,24.748,24.748,24.806,24.831,24.891,24.89,24.875,24.875,24.875,25.006,24.925,24.984,24.954,25.172,25.172,25.172,25.217,25.285,25.348,25.416,25.46,25.46,25.46,25.455,25.429,25.38,25.309,25.354,25.354,25.354,25.367,25.332,25.352,25.363,25.333,25.333,25.333,25.356,25.353,25.359,25.359,25.308,25.308,25.308,25.322,25.272,25.273,25.198,25.166,25.166,25.166,25.2,25.268,25.289,25.243,25.371,25.371,25.371,25.266,25.277,25.319,25.305,25.305,25.305,25.305,25.305,25.361,25.352,25.322,25.286,25.286,25.286,25.354,25.38,25.368,25.392,25.337,25.337,25.337,25.324,25.21,25.228,25.254,25.267,25.267,25.267,25.275,25.256,25.242,25.152,25.164,25.164,25.164,25.175,25.14,25.14,25.09,25.025,25.025,25.025,24.995,25.014,25.036,24.935,24.936,24.936,24.936,24.779,24.75,24.766,24.709,24.722,24.722,24.722,24.748,24.682,24.748,24.738,24.736,24.736,24.736,24.733,24.655,24.731,24.77,24.709,24.709,24.709,24.724,24.755,24.654,24.632,24.577,24.577,24.577,24.636,24.706,24.686,24.699,24.736,24.736,24.736,24.699,24.767,24.91,24.898,24.949,24.949,24.949,24.893,24.819,24.877,24.933,25.025,25.025,25.025,25.048,25.185,25.194,25.143,25.202,25.202,25.202,25.162,25.231,25.379,25.361,25.341,25.341,25.341,25.422,25.352,25.29,25.264,25.261,25.261,25.261,25.231,25.362,25.425,25.389,25.37,25.37,25.37,25.371,25.429,25.457,25.454,25.271,25.271,25.271,25.314,25.286,25.27,25.273,25.234,25.234,25.234,25.178,25.203,25.197,25.2,25.239,25.239,25.239,25.181,25.158,25.11,25.092,25.078,25.078,25.078,25.034,25.034,25.053,25.043,25.031,25.031,25.031,25.045,25.071,25.086,25.027,25.034,25.034,25.034,25.051,25.058,25.088,25.122,25.147,25.147,25.147,25.128,25.125,25.07,25.081,25.078,25.078,25.078,25.098,25.155,25.123,25.159,25.158,25.158,25.158,25.184,25.272,25.316,25.357,25.347,25.347,25.347,25.346,25.313,25.348,25.317,25.293,25.293,25.293,25.263,25.242,25.296,25.256,25.229,25.229,25.229,25.268,25.252,25.235,25.23,25.25,25.25,25.25,25.348,25.372,25.377,25.327,25.33,25.33,25.33,25.288,25.343,25.339,25.262,25.208,25.208,25.208,25.339,25.39,25.295,25.305,25.286,25.286,25.286,25.282,25.284,25.286,25.346,25.358,25.358,25.358,25.295,25.294,25.267,25.271,25.256,25.256,25.256,25.266,25.224,25.196,25.146,25.11,25.11,25.11,25.093,25.098,25.117,25.065,25.019,25.019,25.019,25.036,25.064,25.127,25.127,25.121,25.121,25.121,25.165,25.135,25.135,25.135,25.201,25.201,25.201,25.226,25.185,25.185,25.177,25.166,25.166,25.166,25.154,25.137,25.15,25.116,25.09,25.09,25.09,25.23,25.294,25.218,25.231,25.265,25.265,25.265,25.209,25.178,25.139,25.14,25.097,25.097,25.097,25.102,25.091,25.122,25.121,25.166,25.166,25.166,25.254,25.172,25.136,25.139,25.108,25.108,25.108,25.073,25.101,25.075,25.057,25.043,25.043,25.043,25.056,25.098,25.101,25.073,25.082,25.082,25.082,25.062,24.939,24.948,24.987,25.029,25.029,25.029,25.097,25.025,25.048,25.047,25.016,25.016,25.016,24.957,24.963,25.013,25.093,25.014,25.014,25.014,25.035,25.038,24.994,25.013,24.981,24.981,24.981,24.97,24.884,24.934,24.982,24.96,24.96,24.96,24.962,24.954,24.963,24.972,25.142,25.142,25.142,25.195,25.139,25.168,25.11,25.147,25.147,25.147,25.108,25.064,24.995,25.009,25.009,25.009,25.009,25.009,25.081,25.015,24.977,24.929,24.929,24.929,24.969,24.922,24.92,24.92,24.922,24.922,24.922,24.905,24.96,24.924,24.924,24.946,24.946,24.946,24.98,24.948,24.928,24.922,24.936,24.936,24.936,24.907,24.92,24.867,24.942,24.915,24.915,24.915,24.864,24.907,24.922,24.923,24.918,24.918,24.918,24.899,24.896,24.808,24.812,24.757,24.757,24.757,24.793,24.771,24.784,24.787,24.831,24.831,24.831,24.782,24.8,24.805,24.82,24.83,24.83,24.83,24.862,24.794,24.78,24.785,24.748,24.748,24.748,24.746,24.68,24.663,24.646,24.655,24.655,24.655,24.625,24.648,24.629,24.639,24.667,24.667,24.667,24.648,24.67,24.647,24.639,24.623,24.623,24.623,24.618,24.619,24.616,24.553,24.539,24.539,24.539,24.6,24.609,24.562,24.571,24.588,24.588,24.588,24.582,24.61,24.566,24.523,24.446,24.446,24.446,24.483,24.469,24.471,24.47,24.482,24.482,24.482,24.471,24.457,24.491,24.53,24.565,24.565,24.565,24.544,24.544,24.542,24.557,24.458,24.458,24.458,24.433,24.485,24.446,24.452,24.421,24.421,24.421,24.366,24.326,24.388,24.385,24.318,24.318,24.318,24.327,24.326,24.324,24.301,24.292,24.292,24.292,24.255,24.242,24.29,24.289,24.326,24.326,24.326,24.302,24.335,24.275,24.248,24.277,24.277,24.277,24.308,24.318,24.369,24.326,24.299,24.299,24.299,24.358,24.324,24.28,24.294,24.308,24.308,24.308,24.29,24.314,24.308,24.308,24.336,24.336,24.336,24.323,24.348,24.338,24.378,24.327,24.327,24.327,24.339,24.371,24.365,24.334,24.333,24.333,24.333,24.282,24.288,24.229,24.191,24.198,24.198,24.198,24.169,24.188,24.159,24.179,24.235,24.235,24.235,24.169,24.133,24.168,24.174,24.177,24.177,24.177,24.181,24.161,24.125,24.157,24.21,24.21,24.21,24.268,24.277,24.245,24.22,24.239,24.239,24.239,24.299,24.317,24.346,24.392,24.323,24.323,24.323,24.32,24.321,24.271,24.271,24.271,24.271,24.271,24.281,24.255,24.237,24.237,24.177,24.177,24.177,24.195,24.195,24.29,24.262,24.337,24.337,24.337,24.288,24.253,24.262,24.278,24.286,24.286,24.286,24.29,24.333,24.356,24.31,24.257,24.257,24.257,24.251,24.267,24.294,24.335,24.325,24.325,24.325,24.305,24.312,24.357,24.3
371,24.24,24.24,24.24,24.217,24.257,24.245,24.245,24.263,24.263,24.263,24.259,24.276,24.256,24.245,24.241,24.241,24.241,24.223,24.225,24.226,24.246,24.244,24.244,24.244,24.274,24.378,24.36,24.396,24.419,24.419,24.419,24.399,24.38,24.377,24.406,24.437,24.437,24.437,24.436,24.437,24.467,24.517,24.505,24.505,24.505,24.438,24.477,24.437,24.478,24.542,24.542,24.542,24.555,24.514,24.516,24.54,24.54,24.54,24.54,24.54,24.531,24.38,24.401,24.365,24.365,24.365,24.378,24.349,24.36,24.349,24.317,24.317,24.317,24.289,24.301,24.347,24.353,24.373,24.373,24.373,24.357,24.372,24.382,24.366,24.366,24.366,24.366,24.393,24.38,24.34,24.306,24.304,24.304,24.304,24.333,24.324,24.347,24.302,24.333,24.333,24.333,24.321,24.297,24.322,24.292,24.289,24.289,24.289,24.259,24.257,24.284,24.293,24.282,24.282,24.282,24.285,24.243,24.189,24.218,24.218]; 38window.FX_YEAR_AVG = {"2006":27.485,"2007":27.766,"2008":24.946,"2009":26.435,"2010":25.284,"2011":24.59,"2012":25.149,"2013":25.98,"2014":27.536,"2015":27.279,"2016":27.034,"2017":26.326,"2018":25.647,"2019":25.67,"2020":26.455,"2021":25.64,"2022":24.566,"2023":24.004,"2024":25.12,"2025":24.688,"2026":24.332}; 39</script>
39 40<style> 41@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700;800&family=Onest:wght@600;700;800&family=Inter:wght@400;500;700&display=swap'); 42:root { 43 color-scheme: light; 44 /* Light theme */ 45 --bg: #fafafa; 46 --bg-card: #ffffff; 47 --bg-subtle: #f4f4f5; 48 --bg-hover: #f4f4f5; 49 --bg-banner-info: #fef2f2; 50 --bg-banner-info-border: #fecaca; 51 --bg-banner-success: #f0fdf4; 52 --bg-banner-success-border: #bbf7d0; 53 --bg-banner-error: #fef2f2; 54 --bg-banner-error-border: #fecaca; 55 --text: #18181b; 56 --text-strong: #09090b; 57 --text-muted: #71717a; 58 --text-subtle: #a1a1aa; 59 --border: #e4e4e7; 60 --border-strong: #d4d4d8; 61 --accent: #dc2626; /* primary: red for elektÅina */ 62 --accent-strong: #b91c1c; 63 --accent-bg: #fef2f2; 64 --brand: #00CA85; --brand-strong: #00A26A; --brand-bg: #E1F7EE; /* PKV brand chrome */ 65 --gas: #f59e0b; /* gold for plyn */ 66 --gas-strong: #d97706; 67 --shadow-sm: 0 1px 2px 0 rgba(0,0,0,0.04); 68 --shadow: 0 1px 3px 0 rgba(0,0,0,0.06), 0 1px 2px -1px rgba(0,0,0,0.05); 69 --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.08), 0 4px 6px -4px rgba(0,0,0,0.05); 70 --delta-up: #b91c1c; 71 --delta-down: #00ca85; /* PKV zelená */ 72 --gauge-low: #dc2626; 73 --gauge-mid: #f59e0b; 74 --gauge-high: #00ca85; /* PKV zelená */ 75} 76[data-theme="dark"] { 77 color-scheme: dark; 78 --bg: #171717; 79 --bg-card: #1E1E1E; 80 --bg-subtle: #2A2A2A; 81 --bg-hover: #2A2A2A; 82 --bg-banner-info: #1f1818; 83 --bg-banner-info-border: #7f1d1d; 84 --bg-banner-success: #14201a; 85 --bg-banner-success-border: #166534; 86 --bg-banner-error: #1f1818; 87 --bg-banner-error-border: #7f1d1d; 88 --text: #e4e4e7; 89 --text-strong: #fafafa; 90 --text-muted: #a1a1aa; 91 --text-subtle: #71717a; 92 --border: #333333; 93 --border-strong: #3f3f46; 94 --accent: #ef4444; 95 --accent-strong: #f87171; 96 --accent-bg: #2a1414; 97 --brand: #00CA85; --brand-strong: #4FE0B0; --brand-bg: #14251E; 98 --gas: #fbbf24; 99 --gas-strong: #fcd34d; 100 --shadow-sm: 0 1px 2px 0 rgba(0,0,0,0.3); 101 --shadow: 0 1px 3px 0 rgba(0,0,0,0.35), 0 1px 2px -1px rgba(0,0,0,0.3); 102 --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.4), 0 4px 6px -4px rgba(0,0,0,0.3); 103 --delta-up: #f87171; 104 --delta-down: #34d399; 105} 106* { box-sizing: border-box; } 107html, body { background: var(--bg); } 108table { font-family: 'Inter', system-ui, sans-serif; } 109.big .value, .gauge-info .pct { font-family: 'Onest', 'Open Sans', sans-serif; } 110body { 111 font-family: 'Open Sans', 'Inter', ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 112 color: var(--text); 113 margin: 0; 114 padding: 28px 24px 80px; 115 line-height: 1.55; 116 font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11'; 117 -webkit-font-smoothing: antialiased; 118 transition: background-color 0.2s, color 0.2s; 119} 120.container { max-width: 1800px; margin: 0 auto; } 121header { 122 display: flex; align-items: center; justify-content: space-between; gap: 20px; 123 margin-bottom: 32px; 124} 125.brand { display: flex; flex-direction: column; gap: 4px; } 126h1 { 127 font-size: 26px; margin: 0; color: var(--text-strong); font-weight: 700; 128 letter-spacing: -0.02em; 129} 130.subtitle { color: var(--text-muted); font-size: 14px; } 131.header-actions { display: flex; gap: 10px; align-items: center; } 132.theme-switch { display: inline-flex; align-items: center; gap: 3px; height: 40px; box-sizing: border-box; background: var(--bg-subtle); border: 1px solid var(--border); border-radius: 10px; padding: 3px; } 133.theme-switch button { border: 0; background: transparent; color: var(--text-muted); cursor: pointer; width: 30px; height: 100%; border-radius: 7px; display: inline-flex; align-items: center; justify-content: center; line-height: 0; transition: background .15s ease, color .15s ease; } 134.theme-switch button svg { width: 16px; height: 16px; display: block; } 135.theme-switch button:not(.active):hover { color: var(--text); } 136.theme-switch button.active { background: var(--bg-card); color: var(--text-strong); box-shadow: 0 1px 2px rgba(0,0,0,.08); } 137[data-theme="dark"] .theme-switch button.active { background: rgba(255,255,255,.12); color: #fff; } 138button.icon-btn { 139 background: var(--bg-card); color: var(--text); border: 1px solid var(--border); 140 width: 40px; height: 40px; padding: 0; border-radius: 10px; 141 cursor: pointer; display: flex; align-items: center; justify-content: center; 142 transition: all 0.15s; font-size: 18px; 143} 144button.icon-btn:hover { background: var(--bg-hover); border-color: var(--border-strong); } 145button.refresh { 146 background: var(--brand); color: #fff; border: 0; padding: 0 18px; height: 40px; 147 border-radius: 10px; font-size: 14px; font-weight: 600; cursor: pointer; 148 white-space: nowrap; display: inline-flex; align-items: center; gap: 6px; 149 transition: all 0.15s; 150 box-shadow: var(--shadow-sm); 151} 152button.refresh:hover { background: var(--brand-strong); transform: translateY(-1px); box-shadow: var(--shadow); } 153button.refresh:active { transform: translateY(0); } 154button.refresh:disabled { background: var(--text-subtle); cursor: progress; transform: none; } 155.status-banner { 156 margin: -16px 0 24px; padding: 12px 16px; border-radius: 10px; 157 font-size: 13px; display: none; border: 1px solid; 158} 159.status-banner.info { background: var(--bg-banner-info); color: var(--accent-strong); border-color: var(--bg-banner-info-border); display: block; } 160.status-banner.success { background: var(--bg-banner-success); color: var(--delta-down); border-color: var(--bg-banner-success-border); display: block; } 161.status-banner.error { background: var(--bg-banner-error); color: var(--accent-strong); border-color: var(--bg-banner-error-border); display: block; } 162.freshness { 163 display: inline-block; font-size: 11px; font-weight: 500; padding: 2px 8px; 164 margin-left: 8px; border-radius: 999px; vertical-align: middle; 165 background: var(--bg-banner-info); color: var(--text-subtle); border: 1px solid var(--border); 166} 167.freshness.fresh { background: var(--bg-banner-success); color: var(--delta-down); border-color: var(--bg-banner-success-border); } 168.freshness.aging { background: #fef3c7; color: #92400e; border-color: #fcd34d; } 169.freshness.stale { background: var(--bg-banner-error); color: var(--accent-strong); border-color: var(--bg-banner-error-border); } 170[data-theme="dark"] .freshness.aging { background: #422006; color: #fcd34d; border-color: #78350f; } 171section { 172 background: var(--bg-card); border: 1px solid var(--border); border-radius: 14px; 173 padding: 22px 24px; margin-bottom: 18px; 174 box-shadow: var(--shadow-sm); 175} 176section h2 { 177 margin: 0 0 16px; font-size: 15px; color: var(--text-strong); 178 font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; 179} 180section h2::before { content:''; display:inline-block; width:4px; height:0.8em; background:var(--brand); border-radius:2px; margin-right:9px; vertical-align:-1px; } 181/* Vlastnà legenda grafů 3A/3B â elektÅina a plyn na samostatných ÅádcÃch */ 182.price-legend { display: flex; flex-direction: column; gap: 5px; margin: 2px 0 12px; } 183.price-legend-row { display: flex; flex-wrap: wrap; gap: 16px; align-items: center; } 184.price-legend .pl-item { 185 display: inline-flex; align-items: center; gap: 6px; cursor: pointer; 186 font-size: 12px; font-weight: 500; color: var(--text-muted); user-select: none; 187} 188.price-legend .pl-item.pl-hidden { opacity: 0.35; } 189.price-legend .pl-swatch { width: 14px; height: 4px; border-radius: 2px; display: inline-block; flex: 0 0 auto; } 190table { width: 100%; border-collapse: collapse; font-size: 14px; } 191th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid var(--border); vertical-align: middle; } 192th { 193 background: transparent; font-weight: 500; color: var(--text-muted); 194 font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; 195 padding-bottom: 8px; padding-top: 4px; 196} 197tbody tr:last-child td { border-bottom: 0; } 198tbody tr:hover td { background: var(--bg-subtle); } 199td.num { text-align: right; font-variant-numeric: tabular-nums; font-weight: 500; color: var(--text-strong); } 200.delta-up, .delta-down { 201 font-weight: 700; padding: 3px 10px; border-radius: 6px; 202 display: inline-block; min-width: 140px; text-align: right; 203 font-variant-numeric: tabular-nums; white-space: nowrap; 204 box-sizing: border-box; vertical-align: middle; 205} 206.delta-up { color: var(--delta-up); background: rgba(185, 28, 28, 0.12); } 207.delta-down { color: var(--delta-down); background: rgba(0, 202, 133, 0.14); } 208[data-theme="dark"] .delta-up { background: rgba(248, 113, 113, 0.18); } 209[data-theme="dark"] .delta-down { background: rgba(52, 211, 153, 0.18); } 210/* Velké ÄÃslo v sekci 1/2 nemá pill background, ani min-width */ 211.value.delta-up, .value.delta-down { background: none; padding: 0; min-width: 0; display: inline; text-align: left; }
212/* Mini-badge u velkého ÄÃsla je inline-flex bez ovlivnÄnà layoutu */ 213.value .delta-up, .value .delta-down { min-width: 0; padding: 2px 8px; } 214.delta-flat { color: var(--text-muted); } 215.muted { color: var(--text-muted); font-style: italic; font-weight: 400; } 216.big { 217 display: flex; gap: 28px; flex-wrap: wrap; align-items: baseline; margin-bottom: 14px; 218} 219.big > div { display: flex; flex-direction: column; gap: 2px; } 220.big .value { 221 font-size: 32px; font-weight: 700; color: var(--text-strong); 222 font-variant-numeric: tabular-nums; letter-spacing: -0.02em; 223} 224.big .label { color: var(--text-muted); font-size: 12px; } 225.chart-wrap { position: relative; width: 100%; height: 320px; } 226.chart-wrap-tall { position: relative; width: 100%; height: 400px; } 227.gauge-wrap { display: flex; align-items: center; gap: 28px; flex-wrap: wrap; } 228.gauge-wrap canvas { max-width: 220px; } 229.gauge-info { font-size: 14px; color: var(--text-muted); } 230.gauge-info .pct { font-size: 36px; font-weight: 700; color: var(--text-strong); font-variant-numeric: tabular-nums; } 231.range-controls { 232 display: flex; gap: 4px; flex-wrap: wrap; margin: 10px 0 16px; align-items: center; 233} 234.range-controls .label-small { font-size: 12px; color: var(--text-muted); margin-right: 8px; } 235.range-controls button { 236 background: var(--bg-subtle); color: var(--text-muted); border: 1px solid transparent; 237 padding: 6px 12px; border-radius: 7px; font-size: 12px; cursor: pointer; 238 font-weight: 500; transition: all 0.12s; 239} 240.range-controls button:hover { background: var(--bg-hover); color: var(--text-strong); } 241.range-controls button.active { 242 background: var(--brand); color: #fff; border-color: var(--brand); 243} 244.range-controls button[data-dot] { display: inline-flex; align-items: center; gap: 6px; } 245.range-controls button[data-dot]::before { 246 content: ''; display: inline-block; width: 8px; height: 8px; border-radius: 50%; 247 background: var(--dot-color, currentColor); 248} 249.range-controls button[data-dot]:not(.active) { opacity: 0.55; } 250.range-controls button[data-dot]:not(.active):hover { opacity: 0.85; } 251.range-controls select { font: inherit; font-size: 13px; padding: 6px 8px; border-radius: 8px; border: 1px solid var(--border); background: var(--bg-card); color: var(--text); cursor: pointer; } 252.range-controls select:hover { border-color: var(--border-strong); } 253.date-pickers { margin-left: auto; display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-muted); } 254.date-pickers input[type="date"] { 255 border: 1px solid var(--border); border-radius: 7px; padding: 6px 8px; font-size: 12px; 256 background: var(--bg-card); color: var(--text); 257 font-family: inherit; box-sizing: border-box; min-width: 124px; height: 30px; 258 color-scheme: light dark; line-height: 1; 259} 260/* SjednocenÃ: prázdné i vyplnÄné okno majà STEJNOU barvu textu (ÄÃslice i lomÃtka). 261 Chrome u prázdných polà použÃvá -webkit-text-fill-color pro placeholder â nutno pÅepsat. */ 262.date-pickers input[type="date"]::-webkit-datetime-edit, 263.date-pickers input[type="date"]::-webkit-datetime-edit-fields-wrapper, 264.date-pickers input[type="date"]::-webkit-datetime-edit-text, 265.date-pickers input[type="date"]::-webkit-datetime-edit-month-field, 266.date-pickers input[type="date"]::-webkit-datetime-edit-day-field, 267.date-pickers input[type="date"]::-webkit-datetime-edit-year-field { 268 color: var(--text) !important; 269 -webkit-text-fill-color: var(--text) !important; 270 opacity: 1 !important; 271} 272.date-pickers input[type="date"]::-webkit-calendar-picker-indicator { 273 filter: var(--theme-filter, none); 274} 275[data-theme="dark"] .date-pickers input[type="date"]::-webkit-calendar-picker-indicator { filter: invert(0.8); } 276footer { 277 margin-top: 36px; padding-top: 24px; border-top: 1px solid var(--border); 278 font-size: 12px; color: var(--text-muted); 279} 280footer a { color: var(--text); text-decoration: none; border-bottom: 1px dotted var(--border-strong); } 281footer a:hover { color: var(--brand); border-bottom-color: var(--brand); } 282.sources { margin-top: 8px; } 283.sources a { margin-right: 14px; display: inline-block; margin-bottom: 4px; } 284.note { font-size: 12px; color: var(--text-muted); margin-top: 10px; line-height: 1.5; } 285/* Supabase loading overlay */ 286/* ââ PÅihlaÅ¡ovacà brána âââââââââââââââââââââââââââââââââââââââââââââââ */ 287#auth-gate { position: fixed; inset: 0; z-index: 10000; display: flex; align-items: center; justify-content: center; 288 background: var(--bg, #fff); padding: 20px; } 289#auth-gate[hidden] { display: none; } 290#auth-gate .auth-card { width: 100%; max-width: 380px; background: var(--bg-card); border: 1px solid var(--border); 291 border-radius: 14px; padding: 28px 26px; box-shadow: 0 10px 40px rgba(0,0,0,.18); } 292#auth-gate .auth-brand { font-size: 22px; font-weight: 700; color: var(--text-strong); letter-spacing: -.02em; } 293#auth-gate .auth-sub { font-size: 13px; color: var(--text-muted); margin-top: 4px; margin-bottom: 22px; } 294#auth-gate label { display: block; font-size: 12px; font-weight: 600; color: var(--text-muted); 295 margin: 14px 0 5px; text-transform: uppercase; letter-spacing: .04em; } 296#auth-gate input { width: 100%; padding: 10px 12px; border-radius: 8px; border: 1px solid var(--border); 297 background: var(--bg); color: var(--text-strong); font-size: 14px; box-sizing: border-box; } 298#auth-gate input:focus { outline: 2px solid var(--brand); outline-offset: 1px; } 299#auth-gate button { width: 100%; margin-top: 20px; padding: 11px; border-radius: 8px; border: none; 300 background: var(--brand); color: #fff; font-size: 14px; font-weight: 600; cursor: pointer; } 301#auth-gate button[disabled] { opacity: .6; cursor: default; } 302#auth-gate .auth-oddelovac { display:flex; align-items:center; gap:10px; margin:18px 0 2px; color:var(--text-muted); font-size:11px; text-transform:uppercase; letter-spacing:.05em; } 303#auth-gate .auth-oddelovac::before, #auth-gate .auth-oddelovac::after { content:''; flex:1; height:1px; background:var(--border); } 304#auth-gate .auth-err { margin-top: 14px; padding: 9px 11px; border-radius: 8px; font-size: 13px; 305 background: #fee2e2; color: #991b1b; } 306[data-theme="dark"] #auth-gate .auth-err { background: #450a0a; color: #fca5a5; } 307.auth-who { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-muted); } 308.auth-who a, .auth-who button { font-size: 12px; color: var(--text-muted); background: none; border: none; 309 cursor: pointer; text-decoration: underline; padding: 0; font-family: inherit; } 310#supa-loading { position: fixed; inset: 0; background: var(--bg, #fff); display: flex; align-items: center; justify-content: center; z-index: 9999; transition: opacity 0.3s; } 311#supa-loading.hidden { opacity: 0; pointer-events: none; } 312#supa-loading .spinner { width: 48px; height: 48px; border: 4px solid var(--border); border-top-color: var(--brand); border-radius: 50%; animation: spin 0.8s linear infinite; } 313#supa-loading .msg { margin-left: 16px; color: var(--text-muted); font-size: 14px; } 314@keyframes spin { to { transform: rotate(360deg); } } 315/* News section */ 316.news-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-top: 14px; } 317@media (max-width: 700px) { .news-grid { grid-template-columns: 1fr; } } 318.news-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px; transition: border-color 0.2s, box-shadow 0.2s; } 319.news-card:hover { border-color: var(--brand); box-shadow: var(--shadow-sm); } 320.news-tag { display: inline-block; font-size: 10px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; padding: 2px 8px; border-radius: 999px; background: var(--bg-banner-info); color: var(--accent-strong); margin-bottom: 8px; } 321.news-tag.cz { background: rgba(220, 38, 38, 0.12); color: var(--accent-strong); } 322.news-tag.eu { background: rgba(59, 130, 246, 0.14); color: #1d4ed8; } 323.news-tag.gas { background: rgba(245, 158, 11, 0.14); color: #b45309; } 324.news-tag.oil { background: rgba(124, 58, 237, 0.14); color: #5b21b6; } 325.news-tag.nuclear { background: rgba(16, 185, 129, 0.14); color: #047857; } 326[data-theme="dark"] .news-tag.eu { color: #93c5fd; } 327[data-theme="dark"] .news-tag.gas { color: #fcd34d; } 328[data-theme="dark"] .news-tag.oil { color: #c4b5fd; } 329[data-theme="dark"] .news-tag.nuclear { color: #6ee7b7; } 330.news-card h3 { font-size: 14.5px; font-weight: 600; margin: 0 0 6px; line-height: 1.35; color: var(--text-strong); } 331.news-card .news-summary { font-size: 13px; color: var(--text-muted); line-height: 1.5; margin-bottom: 10px; } 332.news-card .news-meta { font-size: 11px; color: var(--text-subtle); display: flex; gap: 8px; align-items: center; } 333.news-card .news-meta a { color: var(--brand-strong); text-decoration: none; } 334.news-card .news-meta a:hover { text-decoration: underline; } 335.data-disclaimer { 336 background: var(--bg-subtle); border: 1px solid var(--border); color: var(--text-muted); 337 padding: 10px 14px; border-radius: 8px; font-size: 12px; margin-top: 10px; 338 line-height: 1.55; 339} 340/* KPI tiles row */ 341.kpi-row { 342 display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); 343 gap: 12px; margin: 16px 0 4px; 344} 345.kpi { 346 padding: 14px 16px; border-radius: 10px; background: var(--bg-subtle); 347 display: flex; flex-direction: column; gap: 2px; 348} 349.kpi-label { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; } 350.kpi-value { font-size: 22px; font-weight: 700; color: var(--text-strong); font-variant-numeric: tabular-nums; letter-spacing: -0.01em; } 351.kpi-delta { font-size: 12px; font-weight: 500; } 352@media (max-width: 640px) { 353 header { flex-direction: column; align-items: flex-start; } 354 body { padding: 20px 16px 60px; } 355 section { padding: 18px 16px; } 356 .big .value { font-size: 26px; } 357 h1 { font-size: 22px; } 358} 359</style> 360</head> 361<body> 362<div class="container"> 363 364<header> 365 <div style="display:flex;align-items:center;gap:13px"> 366 <svg viewBox="0 0 80 73" width="44" height="40" style="flex:none;display:block" xmlns="http://www.w3.org/2000/svg"><path d="M14.59 57.58S-16.82 45.12 12.53 28.31L55.75 3.56S80.89 -10.84 79.98 18.27L79.02 48.74S77.95 82.71 46.54 70.25Z" fill="#00CA85"></path><path d="M33.8 38.37C33.8 37.05 33.22 35.79 32.5 34.89C31.63 34.16 30.56 33.73 29.38 33.62C28.2 33.73 27.13 34.16 26.26 34.89C25.55 35.79 24.97 37.05 24.97 38.37C24.97 39.72 25.55 40.9 26.26 41.81C27.13 42.54 28.2 43 29.38 43.12C30.55 43 31.63 42.54 32.5 41.81C33.22 40.9 33.8 39.68 33.8 38.37ZM24.94 53.92H20.49V29.79H21.55L23.32 31.89C24.63 30.82 26.63 29.7 29.38 29.15C31.75 29.38 33.89 30.26 35.64 31.68C37.08 33.46 38.24 36.02 38.24 38.37C38.24 41.1 37.08 43.65 35.64 45.16C33.9 46.57 31.75 47.63 29.38 47.63C27.46 47.3 25.98 46.65 24.94 46.08V53.91Z" fill="#fff"></path><path d="M45.13 47.65H40.68V23.23H45.13V38.63L51.73 30.22H56.82L51.07 37.49L56.82 47.65H51.73L48.21 41.31L45.13 45.41V47.65Z" fill="#fff"></path><path d="M63.23 47.65L56.64 30.22H61.65L65.45 41.05L69.21 30.21L74.3 30.22L67.64 47.65H63.23Z" fill="#fff"></path></svg> 367 <div class="brand"> 368 <h1>Energoreport</h1> 369 <div class="subtitle" id="report-date">úterý 19. kvÄtna 2026</div> 370 <div id="data-freshness-header" style="margin-top:6px;font-size:13px;display:flex;align-items:center;gap:8px;flex-wrap:wrap;"></div> 371 </div> 372 </div> 373 <div class="header-actions"> 374 <div class="theme-switch" role="group" aria-label="Motiv zobrazenÃ"> 375 <button type="button" data-mode="light" onclick="setThemeMode('light')" title="SvÄtlý režim" aria-label="SvÄtlý režim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></svg></button> 376 <button type="button" data-mode="system" onclick="setThemeMode('system')" title="Podle systému" aria-label="Podle systému"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg></button> 377 <button type="button" data-mode="dark" onclick="setThemeMode('dark')" title="Tmavý režim" aria-label="Tmavý režim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg></button> 378 </div> 379 <button class="refresh" id="refresh-btn" onclick="refreshData()">
380 <span>â»</span> 381 <span>Aktualizovat data</span> 382 </button> 383 </div> 384</header> 385<nav class="page-nav" style="display:flex;gap:10px;flex-wrap:wrap;margin:4px 0 24px"><a href="index.html" style="background:var(--brand,#00ca85);color:#fff;border:1px solid var(--brand,#00ca85);text-decoration:none;display:inline-flex;align-items:center;gap:7px;padding:8px 15px;border-radius:9px;font-weight:600;font-size:14px;white-space:nowrap"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>PÅehled</a><a href="elektrina.html" style="background:var(--bg-card,#fff);color:var(--text,#3f3f46);border:1px solid var(--border,#e4e4e7);text-decoration:none;display:inline-flex;align-items:center;gap:7px;padding:8px 15px;border-radius:9px;font-weight:600;font-size:14px;white-space:nowrap"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>ElektÅina</a><a href="teplo.html" style="background:var(--bg-card,#fff);color:var(--text,#3f3f46);border:1px solid var(--border,#e4e4e7);text-decoration:none;display:inline-flex;align-items:center;gap:7px;padding:8px 15px;border-radius:9px;font-weight:600;font-size:14px;white-space:nowrap"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h10"/><path d="M9 4v16"/><path d="m3 9 3 3-3 3"/><path d="M12 6 9 9 6 6"/><path d="m6 18 3-3 1.5 1.5"/><path d="M20 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z"/></svg>Teplo</a><a href="plyn.html" style="background:var(--bg-card,#fff);color:var(--text,#3f3f46);border:1px solid var(--border,#e4e4e7);text-decoration:none;display:inline-flex;align-items:center;gap:7px;padding:8px 15px;border-radius:9px;font-weight:600;font-size:14px;white-space:nowrap"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></svg>Plyn</a><a href="pocasi.html" style="background:var(--bg-card,#fff);color:var(--text,#3f3f46);border:1px solid var(--border,#e4e4e7);text-decoration:none;display:inline-flex;align-items:center;gap:7px;padding:8px 15px;border-radius:9px;font-weight:600;font-size:14px;white-space:nowrap"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="M20 12h2"/><path d="m19.07 4.93-1.41 1.41"/><path d="M15.947 12.65a4 4 0 0 0-5.925-4.128"/><path d="M13 22H7a5 5 0 1 1 4.9-6H13a3 3 0 0 1 0 6Z"/></svg>PoÄasà a OZE</a></nav> 386 387<div class="status-banner" id="status-banner"></div> 388<div id="auth-gate" hidden> 389 <form class="auth-card" id="auth-form" autocomplete="on"> 390 <div class="auth-brand">Energoreport</div> 391 <div class="auth-sub">Modul aplikace Energetika â pÅihlaste se firemnÃm úÄtem.</div> 392 <button type="button" id="auth-ms" onclick="authMicrosoft()" style="background:#2f2f2f;display:flex;align-items:center;justify-content:center;gap:9px;margin-top:4px"> 393 <svg width="16" height="16" viewBox="0 0 23 23" aria-hidden="true"><path fill="#f35325" d="M1 1h10v10H1z"/><path fill="#81bc06" d="M12 1h10v10H12z"/><path fill="#05a6f0" d="M1 12h10v10H1z"/><path fill="#ffba08" d="M12 12h10v10H12z"/></svg> 394 PÅihlásit pÅes Microsoft 395 </button> 396 <div class="auth-oddelovac"><span>nebo e-mailem</span></div> 397 <label for="auth-email">E-mail</label> 398 <input id="auth-email" type="email" autocomplete="username" required autofocus /> 399 <label for="auth-pass">Heslo</label> 400 <input id="auth-pass" type="password" autocomplete="current-password" required /> 401 <button type="submit" id="auth-submit">PÅihlásit se</button> 402 <div class="auth-err" id="auth-err" hidden></div> 403 </form> 404</div> 405<div id="supa-loading"><div class="spinner"></div><div class="msg" id="supa-loading-msg">NaÄÃtám data ze Supabaseâ¦</div></div> 406 407<section> 408 <h2>NejdůležitÄjšà zprávy z energetiky <span style="font-size:13px;font-weight:500;color:var(--text-muted);margin-left:8px">poslednÃch 48 hodin</span></h2>
409 <div class="news-grid" id="news-grid"> 410 <!-- zprávy se naÄÃtajà po pÅihlášenà ze Supabase; v souboru zámÄrnÄ nejsou --> 411 </div> 412</section> 413 414<section data-chart="el" data-comm="sel"> 415 <h2>1. ElektÅina â burzovnà ceny a historický vývoj (PXE)</h2> 416 <div class="big"> 417 <div> 418 <div class="value" id="el-current">â EUR/MWh</div> 419 <div class="label">Cal-27 (referenÄnà CZ baseload, <span id="el-current-date">â</span> PXE settlement)</div> 420 </div> 421 </div> 422 <table> 423 <thead> 424 <tr><th>Produkt</th><th>Cena (EUR/MWh)</th><th>Cena (CZK/MWh)</th><th>Dennà zmÄna</th><th>Platnost k</th></tr> 425 </thead> 426 <tbody id="el-table"> 427 <tr data-series="el27"><td>Cal-27 (baseload, dodánà 2027)</td><td class="num">â</td><td class="num">â</td><td class="num">â</td><td class="num">â</td></tr> 428 <tr data-series="el28"><td>Cal-28 (baseload, dodánà 2028)</td><td class="num">â</td><td class="num">â</td><td class="num">â</td><td class="num">â</td></tr> 429 <tr data-series="el29"><td>Cal-29 (baseload, dodánà 2029)</td><td class="num">â</td><td class="num">â</td><td class="num">â</td><td class="num">â</td></tr> 430 </tbody> 431 </table> 432 433 <div class="range-controls"> 434 <span class="label-small">ObdobÃ:</span> 435 <button data-range="1m" class="active" onclick="setPriceRange(this, '1m')">1 mÄsÃc</button> 436 <button data-range="3m" onclick="setPriceRange(this, '3m')">3 mÄsÃce</button> 437 <button data-range="6m" onclick="setPriceRange(this, '6m')">6 mÄsÃců</button> 438 <button data-range="12m" onclick="setPriceRange(this, '12m')">12 mÄsÃců</button> 439 <button data-range="ytd" onclick="setPriceRange(this, 'ytd')">YTD</button> 440 <button data-range="all" onclick="setPriceRange(this, 'all')">VÅ¡e</button> 441 <span class="date-pickers"> 442 <label for="date-from-el">Od:</label> 443 <input type="date" id="date-from-el" onchange="applyCustomRange(this)" /> 444 <label for="date-to-el">Do:</label> 445 <input type="date" id="date-to-el" onchange="applyCustomRange(this)" /> 446 </span> 447 <button data-yzero="el" onclick="togglePriceYZero('el', this)" style="margin-left:8px">ZaÄÃt od 0</button> 448 <button data-ccy-for="el" onclick="toggleCcy('el', this)" title="PÅepnout mÄnu (EUR â CZK)" style="margin-left:8px">KÄ</button> 449 </div> 450 <div class="price-legend" id="price-legend-el"></div> 451 <div class="chart-wrap-tall"><canvas id="price-chart-el"></canvas></div> 452 <div class="note">PXE baseload kontrakty pro dodávku elektÅiny v Äesku (CZ delivery area). Åada <strong>âNext year"</strong> = cena vždy na nejbližšà kalendáÅnà rok (PXE/kurzy.cz, historie od r. 2007), pÅepoÄtená z CZK na EUR/MWh dennÃm referenÄnÃm kurzem ECB. Pro celou historii kliknÄte na <em>VÅ¡e</em>; dalšà Åady (Cal-26/27/28/29) zapnete v legendÄ.</div> 453</section> 454 455<section data-chart="gas" data-comm="sgas"> 456 <h2>2. Zemnà plyn â burzovnà ceny a historický vývoj (PXE / TTF)</h2> 457 <div class="big"> 458 <div> 459 <div class="value" id="gas-current">â EUR/MWh</div> 460 <div class="label">CZ VTP BL Cal-27 (<span id="gas-current-date">â</span> PXE settlement)</div> 461 </div> 462 </div> 463 <table> 464 <thead> 465 <tr><th>Produkt</th><th>Cena (EUR/MWh)</th><th>Cena (CZK/MWh)</th><th>Dennà zmÄna</th><th>Platnost k</th></tr> 466 </thead> 467 <tbody id="gas-table"> 468 <tr data-series="gasTTF"><td>TTF front-month</td><td class="num">â</td><td class="num">â</td><td class="num">â</td><td class="num">â</td></tr> 469 <tr data-series="gas27"><td>CZ VTP BL Cal-27 (dodánà 2027)</td><td class="num">â</td><td class="num">â</td><td class="num">â</td><td class="num">â</td></tr> 470 <tr data-series="gas28"><td>CZ VTP BL Cal-28 (dodánà 2028)</td><td class="num">â</td><td class="num">â</td><td class="num">â</td><td class="num">â</td></tr> 471 <tr data-series="gas29"><td>CZ VTP BL Cal-29 (dodánà 2029)</td><td class="num">â</td><td class="num">â</td><td class="num">â</td><td class="num">â</td></tr> 472 </tbody> 473 </table> 474
475 <div class="range-controls"> 476 <span class="label-small">ObdobÃ:</span> 477 <button data-range="1m" class="active" onclick="setPriceRange(this, '1m')">1 mÄsÃc</button> 478 <button data-range="3m" onclick="setPriceRange(this, '3m')">3 mÄsÃce</button> 479 <button data-range="6m" onclick="setPriceRange(this, '6m')">6 mÄsÃců</button> 480 <button data-range="12m" onclick="setPriceRange(this, '12m')">12 mÄsÃců</button> 481 <button data-range="ytd" onclick="setPriceRange(this, 'ytd')">YTD</button> 482 <button data-range="all" onclick="setPriceRange(this, 'all')">VÅ¡e</button> 483 <span class="date-pickers"> 484 <label for="date-from-gas">Od:</label> 485 <input type="date" id="date-from-gas" onchange="applyCustomRange(this)" /> 486 <label for="date-to-gas">Do:</label> 487 <input type="date" id="date-to-gas" onchange="applyCustomRange(this)" /> 488 </span> 489 <button data-yzero="gas" onclick="togglePriceYZero('gas', this)" style="margin-left:8px">ZaÄÃt od 0</button> 490 <button data-ccy-for="gas" onclick="toggleCcy('gas', this)" title="PÅepnout mÄnu (EUR â CZK)" style="margin-left:8px">KÄ</button> 491 </div> 492 <div class="price-legend" id="price-legend-gas"></div> 493 <div class="chart-wrap-tall"><canvas id="price-chart-gas"></canvas></div> 494 <div class="note">PXE CZ VTP baseload kontrakty zemnÃho plynu (delivery v ÄR). Åada <strong>âNext year"</strong> = cena vždy na nejbližšà kalendáÅnà rok (PXE/kurzy.cz, historie od r. 2015), pÅepoÄtená z CZK na EUR/MWh dennÃm referenÄnÃm kurzem ECB. VÅ¡e v EUR/MWh â vykreslujà se pouze ovÄÅené body, žádná interpolace. Pro celou historii kliknÄte na <em>VÅ¡e</em>.</div> 495</section> 496 497<section data-comm="seua"> 498 <h2>3. Emisnà povolenky â EU ETS I (EUA)</h2> 499 <div class="big"> 500 <div> 501 <div class="value" id="eua-current">â EUR/t</div> 502 <div class="label">EUA · kontrakt s dodánÃm na nejbližšà prosinec · <span id="eua-current-date">â</span> · zdroj oEnergetice</div> 503 </div> 504 </div> 505 <table> 506 <thead> 507 <tr><th>Produkt</th><th>Cena (EUR/t)</th><th>Cena (CZK/t)</th><th>Dennà zmÄna</th><th>Platnost k</th></tr> 508 </thead> 509 <tbody> 510 <tr data-series="eua"><td>EUA â EU ETS I (Dec-front)</td><td class="num">â</td><td class="num">â</td><td class="num">â</td><td class="num">â</td></tr> 511 </tbody> 512 </table> 513 <div class="range-controls" id="eua-range"> 514 <span class="label-small">ObdobÃ:</span> 515 <button data-euarange="3m" onclick="setEuaRange(this,'3m')">3 mÄsÃce</button> 516 <button data-euarange="12m" class="active" onclick="setEuaRange(this,'12m')">12 mÄsÃců</button> 517 <button data-euarange="all" onclick="setEuaRange(this,'all')">VÅ¡e</button> 518 <button data-euayzero onclick="toggleEuaYZero(this)" style="margin-left:8px">ZaÄÃt od 0</button> 519 <button data-euaccy onclick="toggleEuaCcy(this)" title="PÅepnout mÄnu (EUR â CZK)" style="margin-left:8px">KÄ</button> 520 </div> 521 <div class="chart-wrap"><canvas id="eua-chart"></canvas></div> 522 <div class="note">Cena evropské emisnà povolenky (EUA) â hlavnà uhlÃková cena systému <strong>EU ETS I</strong>, klÃÄový driver cen elektÅiny i plynu. Zobrazen kontrakt s dodánÃm na nejbližšà prosinec. Zdroj: <a href="https://oenergetice.cz/energostat/ceny-aktualne/emisni-povolenka" target="_blank" rel="noopener">oEnergetice</a>. (ETS II â nový trh pro budovy a dopravu od 2027 â zatÃm nezobrazujeme, chybà spolehlivý dennà zdroj.)</div> 523</section> 524 525<section data-comm="soil"> 526 <h2>4. Ropa â Brent, WTI (ICE / NYMEX)</h2> 527 <div class="big"> 528 <div> 529 <div class="value" id="oil-current">â USD/bbl</div> 530 <div class="label">Ropa Brent (ICE, pÅednà kontrakt) · <span id="oil-current-date">â</span> · zdroj oEnergetice</div> 531 </div> 532 </div> 533 <table> 534 <thead> 535 <tr><th>Produkt</th><th id="oil-price-header">Cena (USD/bbl)</th><th>Dennà zmÄna</th><th>Platnost k</th></tr> 536 </thead> 537 <tbody> 538 <tr data-oil="brent"><td>Ropa Brent (ICE, evropský benchmark)</td><td class="num">â</td><td class="num">â</td><td class="num">â</td></tr> 539 <tr data-oil="wti"><td>Ropa WTI (NYMEX, americký benchmark)</td><td class="num">â</td><td class="num">â</td><td class="num">â</td></tr> 540 </tbody> 541 </table>
542 <div class="range-controls" id="oil-range"> 543 <span class="label-small">ObdobÃ:</span> 544 <button data-oilrange="1m" onclick="setOilRange(this,'1m')">1 mÄsÃc</button> 545 <button data-oilrange="3m" onclick="setOilRange(this,'3m')">3 mÄsÃce</button> 546 <button data-oilrange="6m" onclick="setOilRange(this,'6m')">6 mÄsÃců</button> 547 <button data-oilrange="12m" class="active" onclick="setOilRange(this,'12m')">12 mÄsÃců</button> 548 <button data-oilrange="ytd" onclick="setOilRange(this,'ytd')">YTD</button> 549 <button data-oilrange="all" onclick="setOilRange(this,'all')">VÅ¡e</button> 550 <button data-oilyzero onclick="toggleOilYZero(this)" style="margin-left:8px">ZaÄÃt od 0</button> 551 <span class="label-small" style="margin-left:12px">Jednotka:</span> 552 <select id="oil-unit" onchange="setOilUnit(this.value)" title="Jednotka ceny ropy"> 553 <option value="usd_bbl">USD/bbl</option> 554 <option value="usd_t">USD/t</option> 555 <option value="usd_gj">USD/GJ</option> 556 <option value="czk_bbl">KÄ/bbl</option> 557 <option value="czk_l">KÄ/l</option> 558 </select> 559 </div> 560 <div class="chart-wrap"><canvas id="oil-chart"></canvas></div> 561 <div class="note">Ceny surové ropy â <strong>Brent</strong> (ICE, evropský/globálnà benchmark) a <strong>WTI</strong> (NYMEX, americký benchmark), pÅednà futures kontrakt v USD za barel. Ropa je klÃÄový vstup pro dopravu i petrochemii a nepÅÃmo ovlivÅuje ceny plynu. Zdroj: <a href="https://oenergetice.cz/energostat/ceny-aktualne/ropa-brent" target="_blank" rel="noopener">oEnergetice</a>.</div> 562</section> 563 564<section data-comm="s4"> 565 <h2>5. Spotová cena elektÅiny â OTE dennà trh (15 min)</h2> 566 <div class="big"> 567 <div> 568 <div class="value" id="ote-base">â EUR/MWh</div> 569 <div class="label" id="ote-base-label">Base Load</div> 570 </div> 571 <div> 572 <div class="value" id="ote-peak" style="font-size:22px">110,44</div> 573 <div class="label">Peak (8â20 h)</div> 574 </div> 575 <div> 576 <div class="value" id="ote-offpeak" style="font-size:22px">154,85</div> 577 <div class="label">Offpeak</div> 578 </div> 579 </div> 580 <div class="range-controls"> 581 <span class="label-small">Den:</span> 582 <input type="date" id="ote-date-picker" onchange="drawOteChart()" /> 583 <button onclick="setOteDate(-1)" style="font-size:14px;line-height:1">â¹</button> 584 <button onclick="setOteDate(1)" style="font-size:14px;line-height:1">âº</button> 585 <button onclick="setOteDate(0)">Dnes</button> 586 <span id="ote-date-note" style="margin-left:12px;color:var(--text-muted);font-size:12px"></span> 587 <button data-ccy-for="ote" onclick="toggleCcy('ote', this)" title="PÅepnout mÄnu (EUR â CZK)" style="margin-left:8px">KÄ</button> 588 </div> 589 <div class="chart-wrap"><canvas id="ote-chart"></canvas></div> 590 <div class="note">15-minutová spotová cena (15min produkt) v EUR/MWh. Plnou Äarou aktuálnÄ vybraný den, ÄárkovanÄ D+1 (zÃtÅek) pokud je zveÅejnÄn. OTE publikuje D+1 každý den po obÄdÄ po sesouhlasenÃ. Databáze obsahuje <span id="ote-history-count">2</span> dnů à 96 hodnot; scheduled task pÅidává každý den nový profil + zÃtÅek pokud dostupný.</div> 591</section> 592 593<section data-comm="s5"> 594 <h2>6. Hodinové spotové ceny â historie 2020-2026</h2> 595 <div class="range-controls"> 596 <span class="label-small">ObdobÃ:</span> 597 <button data-spot-range="7d" onclick="setSpotRange('7d')">7 dnÃ</button> 598 <button data-spot-range="30d" onclick="setSpotRange('30d')">30 dnÃ</button> 599 <button data-spot-range="90d" onclick="setSpotRange('90d')">90 dnÃ</button> 600 <button data-spot-range="1y" class="active" onclick="setSpotRange('1y')">1 rok</button> 601 <button data-spot-range="ytd" onclick="setSpotRange('ytd')">YTD</button> 602 <button data-spot-range="3y" onclick="setSpotRange('3y')">3 roky</button> 603 <button data-spot-range="all" onclick="setSpotRange('all')">VÅ¡e (6 let)</button> 604 <button data-spot-range="compare" onclick="setSpotRange('compare')">Srovnánà let</button> 605 <span class="date-pickers"> 606 <label for="spot-from">Od:</label> 607 <input type="date" id="spot-from" onchange="applySpotCustomRange()" /> 608 <label for="spot-to">Do:</label> 609 <input type="date" id="spot-to" onchange="applySpotCustomRange()" /> 610 </span> 611 <button data-ccy-for="spot5" onclick="toggleCcy('spot5', this)" title="PÅepnout mÄnu (EUR â CZK)" style="margin-left:8px">KÄ</button> 612 </div> 613 <div class="chart-wrap-tall">
613<canvas id="spot-hourly-chart"></canvas></div> 614 <div class="note">Hodinová marginálnà cena ÄR (EUR/MWh) z OTE dennÃho trhu. V režimu <strong>Srovnánà let</strong> jsou roky pÅekryté podle dne v roce; osa je proto spoleÄná nepÅestupná Å¡kála, kdežto tooltip ukazuje <strong>skuteÄné datum dané kÅivky</strong> â v pÅestupných letech (2020, 2024) se tak od bÅezna lišà o den. <span id="spot-stats"></span>. Zdroj: historický základ z Excelu 260408 pkv (od 2020-01-01) + dennà automatické doplÅovánà z OTE. Data jsou v Supabase a aktualizujà se dennÄ.</div> 615</section> 616 617<section data-comm="s20"> 618 <h2>7. KoneÄná spotÅeba energie ÄR podle nosiÄů</h2> 619 <div data-ai-col> 620 <div class="range-controls" id="fec-controls"> 621 <span class="label-small">ZobrazenÃ:</span> 622 <button data-fecview="pj" class="active" onclick="setFecView('pj')">Objem (PJ)</button> 623 <button data-fecview="pct" onclick="setFecView('pct')">Struktura (%)</button> 624 <span class="label-small" style="margin-left:12px" id="fec-scen-label" hidden>Výhled:</span> 625 <span id="fec-scen-buttons"></span> 626 </div> 627 <div class="chart-wrap-tall"><canvas id="fec-chart"></canvas></div> 628 </div> 629 <div style="overflow-x:auto;margin-top:14px"> 630 <table id="fec-table" style="font-size:12px;min-width:900px"> 631 <thead><tr id="fec-thead"></tr></thead> 632 <tbody id="fec-tbody"></tbody> 633 </table> 634 </div> 635 <div class="note" id="fec-note-def">Sloupce = <strong>koneÄná spotÅeba energie</strong> (energetické užitÃ) v ÄR podle nosiÄe energie, v PJ za rok. Zdroj: <strong>Eurostat</strong>, energetické bilance <a href="https://ec.europa.eu/eurostat/databrowser/view/nrg_bal_c/default/table?lang=en" target="_blank" rel="noopener">nrg_bal_c</a>, ukazatel <code>FC_E</code> (Final consumption â energy use), staženo pÅes SDMX API v TJ a pÅepoÄteno na PJ. Rozklad na nosiÄe je nepÅekrývajÃcà se a jeho souÄet <strong>pÅesnÄ odpovÃdá</strong> souÄtu Eurostatu za vÅ¡echny roky.</div> 636 <div class="note" id="fec-note-outlook">ð <strong>Výhled 2025â2050</strong> pocházà z modelu <strong>TIMES-CZ</strong> (projekt SEEPIA), tedy ze stejného modelovánÃ, které stojà za Grafem Ä. 11 v NKEP â scénáÅe <strong>WEM</strong> (se stávajÃcÃmi opatÅenÃmi) a <strong>WAM</strong> (s dodateÄnými opatÅenÃmi). Samotný NKEP tato ÄÃsla publikuje <strong>pouze jako obrázek</strong>; ÄÃselnÄ je uvádà Tab. 1 odborné analýzy ÄVUT FS pro Ministerstvo financà (<a href="https://mf.gov.cz/assets/attachments/2026-03-27_Priloha-k-Info-106-99-MF-12889-2026-74.pdf" target="_blank" rel="noopener">PDF na mf.gov.cz</a>), odkud jsou pÅevzata. SouÄty za rok 2050 (WEM 955,0 PJ / WAM 752,4 PJ) souhlasà s grafem v NKEP. â Tatáž analýza ÄVUT <strong>modelovánà SEEPIA kritizuje</strong> â uvádÃ, že tuzemská netto spotÅeba vycházà v modelu o 7,1 % nÞe než ve statistice ERÃ. Ber výhled jako <strong>scénáŠjednoho modelu</strong>, ne jako pÅedpovÄÄ.</div> 637 <div class="note" id="fec-note-granularity">â± <strong>Model poÄÃtá v pÄtiletých krocÃch</strong> (2025, 2030, 2035, 2040, 2045, 2050 â dokumentace TIMES-CZ v03.0). RoÄnà hodnoty pro budoucnost <strong>neexistujÃ</strong>. Osa grafu je proto roÄnÃ, ale sloupce jsou jen v modelových letech; tlaÄÃtkem âDopoÄÃtat lineárnÄ" lze mezilehlé roky doplnit â pak jsou <strong>svÄtlejšà a v tabulce oznaÄené jako âdopoÄet"</strong>, protože to nejsou data z modelu, ale prostá interpolace mezi jeho výsledky.</div> 638 <div class="note" id="fec-note-compare">ð <strong>SkuteÄnost a výhled nejsou totožnÄ vymezené.</strong> Historie je z Eurostatu (<code>FC_E</code>), výhled z TIMES-CZ v metodice energetické bilance EK. Pro rok 2019, kde se obojà pÅekrývá, dává Eurostat <strong>1 014,4 PJ</strong> a model <strong>1 029,5 PJ</strong> (+1,5 %). Model také nevede biopaliva v dopravÄ jako samostatnou položku (Eurostat ano, 2024: 14,9 PJ) a âKoks" u nÄj neobsahuje vyrobené plyny. RozdÃl v úrovni si pÅi Ätenà pÅechodu 2024 â 2025 odeÄti.</div> 639 <div class="note" id="fec-note-caveats">â Co v ÄÃslech <strong>nenÃ</strong>: neenergetické užità (suroviny pro chemii, maziva, asfalt) â ukazatel <code>FC_E</code> poÄÃtá jen energii spotÅebovanou jako energii. ElektÅina a teplo jsou vedeny jako <strong>nosiÄe u koneÄného spotÅebitele</strong>, ne podle paliva, ze kterého vznikly â palivo na jejich výrobu je v transformaci, mimo tento ukazatel. âEnergie prostÅedÃ" je teplo odebrané tepelnými Äerpadly, âOdpad" jen nerecyklovatelná (neobnovitelná) Äást.</div> 640</section> 641 642<footer> 643 <strong>Zdroje:</strong> 644 <div class="sources"> 645 <a href="https://oenergetice.cz/energostat/ceny-aktualne/elektrina" target="_blank">oEnergetice â cena elektÅiny</a> 646 <a href="https://oenergetice.cz/energostat/ceny-aktualne/zemni-plyn" target="_blank">oEnergetice â cena plynu</a> 647 <a href="https://www.ote-cr.cz/cs/kratkodobe-trhy/elektrina/denni-trh" target="_blank">OTE â dennà trh elektÅiny</a> 648 <a href="https://agsi.gie.eu/" target="_blank">AGSI+ â zásobnÃky plynu</a> 649 <a href="https://www.zasobyplynu.cz/" target="_blank">zasobyplynu.cz</a> 650 <a href="https://pxe.cz/cs/derivatovy-trh/elektrina" target="_blank">PXE Market Data Hub</a> 651 <a href="https://www.kurzy.cz/komodity/elektrina-graf-vyvoje-ceny/" target="_blank">kurzy.cz â elektÅina</a> 652 <a href="https://www.kurzy.cz/komodity/pxe-zemni-plyn-graf-vyvoje-ceny/" target="_blank">kurzy.cz â plyn</a> 653 <a href="https://eru.gov.cz/prehled-cen-tepelne-energie-v-cleneni-podle-cenovych-lokalit" target="_blank">ERà â výsledné ceny tepla</a>
654 <a href="https://www.eex.com/en/market-data/market-data-hub" target="_blank">EEX Market Data Hub</a> 655 </div> 656 <div class="note" id="generated-at">Vygenerováno: 19. kvÄtna 2026.</div> 657</footer> 658 659</div> 660
661<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.js" integrity="sha384-iU8HYtnGQ8Cy4zl7gbNMOhsDTTKX02BTXptVP/vqAWIaTfM7isw76iyZCsjL2eVi" crossorigin="anonymous"></script>
vendor: 1 bytes, line 661
661
662<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/index.umd.min.js"></script>
vendor: 1 bytes, line 662
662
663<script src="https://cdn.jsdelivr.net/npm/[email protected]/hammer.min.js"></script>
vendor: 1 bytes, line 663
663
664<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chartjs-plugin-zoom.min.js"></script>
vendor: 1 bytes, line 664
664
665<script> 666// ===== Zoom v Äárových grafech: táhnutÃm pÅiblÞÃÅ¡, Ctrl+koleÄko, Shift+táhnutà = posun, dvojklik = reset ===== 667(function () { 668 if (!window.Chart) return; 669 // 1) registruj zoom plugin (pokud se build nezaregistroval sám) 670 try { 671 if (Chart.registry && !Chart.registry.plugins.get('zoom')) { 672 var zp = window.ChartZoom || window['chartjs-plugin-zoom'] || window.chartjsPluginZoom; 673 if (zp) Chart.register(zp.default || zp); 674 } 675 } catch (e) { console.warn('zoom register failed', e); } 676 677 var ZOOM_CFG = { 678 zoom: { 679 drag: { enabled: true, backgroundColor: 'rgba(0,202,133,0.15)', borderColor: 'rgba(0,202,133,0.75)', borderWidth: 1 }, 680 wheel: { enabled: true, modifierKey: 'ctrl', speed: 0.08 }, 681 pinch: { enabled: true }, 682 mode: 'x' 683 }, 684 pan: { enabled: true, mode: 'x', modifierKey: 'shift' }, 685 limits: { x: { min: 'original', max: 'original' } } 686 }; 687 688 // 2) zoom Äte config pÅi vytvoÅenà grafu â vlož ho do configu Äárových grafů obalenÃm konstruktoru 689 var _Chart = window.Chart; 690 function PatchedChart(item, config) { 691 try { 692 if (config && config.type === 'line') { 693 config.options = config.options || {}; 694 config.options.plugins = config.options.plugins || {}; 695 if (!config.options.plugins.zoom) config.options.plugins.zoom = ZOOM_CFG; 696 } 697 } catch (e) {} 698 return new _Chart(item, config); 699 } 700 PatchedChart.prototype = _Chart.prototype; 701 Object.setPrototypeOf(PatchedChart, _Chart); // Chart.register/getChart/defaults/registry projdou na originál 702 window.Chart = PatchedChart; 703 704 // 3) dvojklik na graf = reset pÅiblÞenà 705 document.addEventListener('dblclick', function (e) { 706 if (!e.target || e.target.tagName !== 'CANVAS') return; 707 var ch = _Chart.getChart(e.target); 708 if (ch && typeof ch.resetZoom === 'function') ch.resetZoom(); 709 }); 710})(); 711</script>
vendor: 1 bytes, line 711
711
712<script> 713// ===== Motiv: SvÄtlý / Podle systému / Tmavý ===== 714function effectiveTheme(mode) { 715 const m = mode || window.__themeMode || 'system'; 716 if (m === 'light' || m === 'dark') return m; 717 return (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light'; 718} 719function redrawChartsForTheme() { 720 // PÅekreslenà grafů s barvami motivu (každé volánà izolované: graf z jiné stránky nesmà shodit ostatnÃ) 721 const __rt = fn => { try { fn(); } catch (e) {} }; 722 __rt(() => { if (typeof drawPriceChart === 'function') { drawPriceChart('el', priceChartState.el.range); drawPriceChart('gas', priceChartState.gas.range); } }); 723 __rt(() => { if (typeof drawStorageHistoryChart === 'function') drawStorageHistoryChart(); }); 724 __rt(() => { if (typeof drawOteChart === 'function') drawOteChart(); }); 725 __rt(() => { if (typeof drawStorageGauge === 'function') drawStorageGauge(window.__lastStoragePct || 33); }); 726 __rt(() => { if (typeof drawHeatChart === 'function') drawHeatChart(); }); 727 __rt(() => { if (typeof drawTapChart === 'function') drawTapChart(); }); 728 __rt(() => { if (typeof drawSpotHourlyChart === 'function') { if (__spotRange === 'compare') drawSpotCompareChart(); else drawSpotHourlyChart(); } }); 729 __rt(() => { if (typeof drawSpotHourChart === 'function') drawSpotHourChart(); }); 730 __rt(() => { if (typeof drawSpotDowChart === 'function') drawSpotDowChart(); }); 731 __rt(() => { if (typeof drawForwardSpotChart === 'function') drawForwardSpotChart(); }); 732 __rt(() => { if (typeof drawBalancingChart === 'function') drawBalancingChart(); }); 733 __rt(() => { if (typeof drawEuStorage === 'function') drawEuStorage(); }); 734 __rt(() => { if (typeof drawEuaChart === 'function') drawEuaChart(); }); 735 __rt(() => { if (typeof drawFecChart === 'function') drawFecChart(); }); 736 __rt(() => { if (typeof drawOilChart === 'function') drawOilChart(); }); 737} 738function syncThemeSwitch() { 739 const mode = window.__themeMode || 'system'; 740 document.querySelectorAll('.theme-switch [data-mode]').forEach(b => { 741 const on = b.getAttribute('data-mode') === mode; 742 b.classList.toggle('active', on); 743 b.setAttribute('aria-pressed', on ? 'true' : 'false'); 744 }); 745} 746function applyTheme(redraw) { 747 const prev = document.documentElement.getAttribute('data-theme'); 748 const eff = effectiveTheme(); 749 document.documentElement.setAttribute('data-theme', eff); 750 syncThemeSwitch(); 751 if (redraw && prev && prev !== eff) redrawChartsForTheme(); 752} 753function setThemeMode(mode) { 754 window.__themeMode = mode; 755 try { localStorage.setItem('themeMode', mode); } catch (e) {} 756 applyTheme(true); 757} 758// ZpÄtná kompatibilita se starým volánÃm 759function toggleTheme() { setThemeMode(effectiveTheme() === 'dark' ? 'light' : 'dark'); } 760(function initTheme() { 761 let mode = null; 762 try { mode = localStorage.getItem('themeMode'); } catch (e) {} 763 if (!mode) { 764 let old = null; try { old = localStorage.getItem('theme'); } catch (e) {} // migrace ze starého klÃÄe 765 mode = (old === 'light' || old === 'dark') ? old : 'system'; 766 } 767 window.__themeMode = mode; 768 document.documentElement.setAttribute('data-theme', effectiveTheme()); 769 syncThemeSwitch(); 770 if (window.matchMedia) { 771 try { 772 window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () { 773 if ((window.__themeMode || 'system') === 'system') applyTheme(true); // v režimu âPodle systému" reaguj na OS 774 }); 775 } catch (e) {} 776 } 777})(); 778function getCssVar(name) { 779 return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); 780} 781 782// Adaptivnà popisky osy X podle rozsahu (i pÅi zoomu): dny â âd. M.", týdny/mÄsÃce â âmÄs rr", roky â rok 783function adaptiveXTick(value) { 784 var span = (this && isFinite(this.max) && isFinite(this.min)) ? (this.max - this.min) : Infinity; 785 var days = span / 86400000; 786 var d = new Date(value); 787 if (days <= 90) return d.toLocaleDateString('cs-CZ', { day: 'numeric', month: 'numeric' }); // dny â â12. 2." 788 if (days <= 1400) return d.toLocaleDateString('cs-CZ', { month: 'short', year: '2-digit' }); // mÄsÃce â âúno 26" 789 return String(d.getFullYear()); // dlouhé â rok 790} 791
792// KalendáÅnÄ zarovnané popisky osy X (1. dne mÄsÃce/roku) â rovnomÄrné rozestupy bez prázdných mÃst 793// Uložà skuteÄné meze dat; pÅi zoomu jsou to meze výÅezu â zoom zůstává funkÄnà 794function captureXDataLimits(scale) { scale.$dMin = scale.min; scale.$dMax = scale.max; } 795function calendarXTicks(scale) { 796 try { 797 // osa pÅesnÄ v rozsahu dat â jinak ji Chart.js roztáhne na zaokrouhlené ticky a vzniknou mezery po krajÃch 798 if (isFinite(scale.$dMin) && isFinite(scale.$dMax) && scale.$dMax > scale.$dMin) { scale.min = scale.$dMin; scale.max = scale.$dMax; } 799 var min = scale.min, max = scale.max; 800 if (!isFinite(min) || !isFinite(max) || max <= min) return; 801 var days = (max - min) / 86400000, out = [], d, t, step; 802 if (days <= 90) { 803 step = Math.max(1, Math.round(days / 8)); 804 d = new Date(min); d.setHours(0, 0, 0, 0); 805 if (d.getTime() < min) d.setDate(d.getDate() + 1); 806 for (t = d.getTime(); t <= max; t += step * 86400000) out.push({ value: t }); 807 } else if (days <= 1400) { 808 step = days > 900 ? 3 : (days > 450 ? 2 : 1); 809 d = new Date(min); d.setDate(1); d.setHours(0, 0, 0, 0); 810 if (d.getTime() < min) d.setMonth(d.getMonth() + 1); 811 while (d.getTime() <= max) { out.push({ value: d.getTime() }); d.setMonth(d.getMonth() + step); } 812 } else { 813 var years = days / 365.25; 814 step = years > 12 ? 3 : (years > 6 ? 2 : 1); 815 d = new Date(min); d.setMonth(0, 1); d.setHours(0, 0, 0, 0); 816 if (d.getTime() < min) d.setFullYear(d.getFullYear() + 1); 817 while (d.getTime() <= max) { out.push({ value: d.getTime() }); d.setFullYear(d.getFullYear() + step); } 818 } 819 // ProÅÃdnutà podle Å ÃÅKY osy â jinak se v úzkém grafu (napÅ. vedle mapy) popisky pÅekrývajÃ. 820 if (out.length > 2) { 821 var lblW = 0; 822 try { 823 var c = scale.ctx; 824 if (c) { 825 c.save(); 826 var fs = (scale.options.ticks.font && scale.options.ticks.font.size) || 11; 827 c.font = fs + 'px system-ui, -apple-system, sans-serif'; 828 var stepS = Math.max(1, Math.floor(out.length / 5)); 829 for (var i = 0; i < out.length; i += stepS) lblW = Math.max(lblW, c.measureText(adaptiveXTick.call(scale, out[i].value)).width); 830 c.restore(); 831 } 832 } catch (e) { /* mÄÅenà selhalo â použij konzervativnà odhad nÞe */ } 833 var need = (lblW || 62) + 14; 834 var avail = scale.width || (scale.chart && scale.chart.width * 0.8) || 600; 835 var maxT = Math.max(2, Math.floor(avail / need)); 836 if (out.length > maxT) { 837 var k = Math.ceil(out.length / maxT); 838 out = out.filter(function (_, i) { return i % k === 0; }); 839 } 840 } 841 if (out.length) scale.ticks = out; // Chart.js v4: ticky se mÄnà mutacÃ, návratová hodnota se ignoruje 842 } catch (e) { /* pÅi chybÄ ponech výchozà ticky â osa se nesmà rozbÃt */ } 843} 844function paletteColor(c) { 845 // SvÄtlé téma: zelené odstÃny v grafech sjednoceny na PKV zelenou (#00ca85) 846 if ((document.documentElement.getAttribute('data-theme') || 'light') !== 'light') return c; 847 return (c === '#10b981' || c === '#16a34a' || c === '#047857') ? '#00ca85' : c; 848} 849function updateThemeIcon() { syncThemeSwitch(); } 850</script>
vendor: 1 bytes, line 850
850
851<script> 852// ===== OTE 15min spot data â databáze 15min profilů ===== 853// Key: YYYY-MM-DD ISO date. Value: array of 96 Ätvrthodinových cen (15min produkt) v EUR/MWh. 854// Scheduled task pÅidává každý den nový profil + zÃtÅek pokud zveÅejnÄn. 855window.ote15Data = {}; 856// Generuj Äasové popisky 00:00, 00:15, ..., 23:45 857const ote15Hours = []; 858for (let h = 0; h < 24; h++) { 859 for (let m of [0, 15, 30, 45]) { 860 ote15Hours.push(String(h).padStart(2,'0') + ':' + String(m).padStart(2,'0')); 861 } 862} 863// Default zobrazený den 864const oteData = { 865 date: Object.keys(window.ote15Data).sort().reverse()[0] || "2026-05-18", 866 hours: ote15Hours, 867 prices15: null 868}; 869oteData.prices15 = window.ote15Data[oteData.date]; 870// Dny, které majà reálná data (â¥1 platná hodnota) â kvůli mezerám v 15min datech 871function oteAvailableDays() { 872 return Object.keys(window.ote15Data || {}) 873 .filter(d => Array.isArray(window.ote15Data[d]) && window.ote15Data[d].some(v => v !== null && v !== undefined && isFinite(v))) 874 .sort(); 875} 876function setOteDate(offset) { 877 const picker = document.getElementById('ote-date-picker'); 878 if (!picker) return; 879 const days = oteAvailableDays(); 880 if (!days.length) { drawOteChart(); return; } 881 if (offset === 0) { 882 picker.value = days[days.length-1]; // poslednà dostupný den 883 } else { 884 // Posun jen mezi dostupnými dny (pÅeskoÄ mezery) 885 let idx = days.indexOf(picker.value); 886 if (idx < 0) idx = days.length - 1; 887 idx = Math.min(days.length - 1, Math.max(0, idx + offset)); 888 picker.value = days[idx]; 889 } 890 drawOteChart(); 891} 892 893// ===== NaÄti uložené OTE dny z localStorage pÅi startu ===== 894(function loadCachedOte() { 895 try { 896 const cached = localStorage.getItem('ote15DataCache'); 897 if (cached) { 898 const obj = JSON.parse(cached); 899 Object.assign(window.ote15Data, obj); 900 } 901 } catch(e) { console.warn('OTE cache load failed', e); } 902})(); 903 904function saveOteCache() { 905 try { 906 localStorage.setItem('ote15DataCache', JSON.stringify(window.ote15Data)); 907 } catch(e) { console.warn('OTE cache save failed', e); } 908} 909 910// ===== Auto-fetch 15min OTE data pro daný den ===== 911async function loadOteFromCR(date) { 912 if (!window.cowork || !window.cowork.callMcpTool) { 913 return { ok: false, error: 'Cowork MCP nenà dostupný (artefakt mimo Cowork prostÅedÃ).' }; 914 } 915 const url = `https://www.ote-cr.cz/cs/kratkodobe-trhy/elektrina/denni-trh?time_resolution=PT15M&date=${date}`; 916 try { 917 const html = await window.cowork.callMcpTool('mcp__workspace__web_fetch', {url}); 918 // Sparsuj 15min tabulku pÅÃmo (rychlejšà než askClaude) 919 const doc = new DOMParser().parseFromString(String(html), 'text/html'); 920 let arr = []; 921 for (const tbl of doc.querySelectorAll('table')) { 922 const rows = tbl.querySelectorAll('tr'); 923 if (rows.length >= 90 && rows.length <= 100) { 924 for (let i = 1; i < rows.length; i++) { 925 const cells = Array.from(rows[i].querySelectorAll('td')).map(c => c.textContent.trim()); 926 if (cells.length < 2 || !/^\d\d:\d\d-\d\d:\d\d$/.test(cells[0])) continue; 927 const p = parseFloat(cells[1].replace(/\s/g, '').replace(',', '.')); 928 if (isFinite(p)) arr.push(p); 929 } 930 break; 931 } 932 } 933 if (arr.length !== 96) { 934 // Fallback: zkus askClaude 935 const prompt = `Z následujÃcÃho HTML stránky OTE-CR pro den ${date} vytáhni 96 15-minutových cen elektÅiny (15min produkt) v EUR/MWh. VraÅ¥ POUZE JSON pole 96 ÄÃsel, bez vysvÄtlenÃ. Pokud nedostupné, []`; 936 const result = await window.cowork.askClaude(prompt, [String(html)]); 937 const match = String(result).match(/\[[\s\S]*?\]/); 938 if (match) try { arr = JSON.parse(match[0]); } catch(e) {} 939 } 940 if (!Array.isArray(arr) || arr.length !== 96) { 941 return { ok: false, error: `OTE pro ${date} nevrátil 96 hodnot (naÅ¡lo se ${arr.length}). Den asi nenà zveÅejnÄný.` }; 942 } 943 if (arr.some(v => typeof v !== 'number' || !isFinite(v))) { 944 return { ok: false, error: 'NÄkteré hodnoty nejsou ÄÃsla.' }; 945 } 946 window.ote15Data[date] = arr; 947 saveOteCache(); 948 return { ok: true, count: 96 }; 949 } catch (e) { 950 return { ok: false, error: e.message || String(e) }; 951 } 952} 953 954// ===== DATABÃZE â pouze ovÄÅené body z veÅejných zdrojů ===== 955// Tento JSON je perzistentnà úložiÅ¡tÄ. Scheduled task ho pÅi každém spuÅ¡tÄnÃ: 956// 1. parsuje (regex nebo DOMParser), 957// 2. pÅidá dneÅ¡nà hodnoty, 958// 3. zapÃÅ¡e zpÄt do HTML.
959// Žádná interpolace ani odhady â pokud data nejsou, prostÄ se nezobrazÃ. 960 961const HISTORICAL_DATA = {series: {el26:{label:"ElektÅina Cal-26",points:[]}, el27:{label:"ElektÅina Cal-27",points:[]}, el28:{label:"ElektÅina Cal-28",points:[]}, el29:{label:"ElektÅina Cal-29",points:[]}, el_nextyear:{label:"ElektÅina Next year",points:[]}, gasTTF:{label:"Plyn TTF",points:[]}, gas27:{label:"Plyn CZ VTP Cal-27",points:[]}, gas28:{label:"Plyn CZ VTP Cal-28",points:[]}, gas29:{label:"Plyn CZ VTP Cal-29",points:[]}, gas_nextyear:{label:"Plyn Next year",points:[]}, eua:{label:"Emisnà povolenky EU ETS (EUA)",points:[]}, brent:{label:"Ropa Brent",points:[]}, wti:{label:"Ropa WTI",points:[]}}}; 962 963// Spojà Cal-X kontrakty podle aktivnÃho roku do jedné âfront year" série. 964// Pravidlo: bÄhem kalendáÅnÃho roku N je ânext year" kontrakt s dodávkou v roce N+1. 965// PÅÃklady: 2025-06-15 â el26 (Cal-26 = delivery 2026), 2026-03-01 â el27, ⦠966function computeNextYearSeries(prefix) { 967 const points = []; 968 Object.entries(HISTORICAL_DATA.series).forEach(([sid, series]) => { 969 const m = sid.match(/^(el|gas)(\d{2})$/); 970 if (!m || m[1] !== prefix) return; 971 const deliveryYear = 2000 + parseInt(m[2]); 972 const activeYear = deliveryYear - 1; // rok, ve kterém je tento kontrakt ânext year" 973 series.points.forEach(p => { 974 const py = parseInt(p.date.substr(0, 4)); 975 if (py === activeYear) points.push({ date: p.date, value: p.value, from: sid }); 976 }); 977 }); 978 points.sort((a, b) => a.date.localeCompare(b.date)); 979 return points; 980} 981// ===== Historická spotová data (Excel 260408 pkv Analýza spotových cen) ===== 982// 53 405 hodinových bodů 2020-01-01 až 2026-04-05, marginálnà cena ÄR (EUR/MWh). 983window.spotData = {prices:[], firstTs: 1577836800, firstDate: "2020-01-01 00:00", lastDate: "", totalHours: 0, avgByHour3yr: [], avgByHourByYear: {}, avgByDow3yr: [], avgByDowByYear: {}, dailyByYear: {}}; 984const TODAY = new Date(); 985 986// Pomocné funkce nad HISTORICAL_DATA 987function getSeriesPoints(seriesId) { 988 const s = HISTORICAL_DATA.series[seriesId]; 989 return s ? s.points : []; 990} 991 992function rangeStart(range) { 993 let start = new Date(TODAY); 994 if (range === '1m') start.setMonth(start.getMonth() - 1); 995 else if (range === '3m') start.setMonth(start.getMonth() - 3); 996 else if (range === '6m') start.setMonth(start.getMonth() - 6); 997 else if (range === '12m') start.setFullYear(start.getFullYear() - 1); 998 else if (range === 'ytd') start = new Date(TODAY.getFullYear(), 0, 1); 999 else if (range === 'all') start = new Date('2020-01-01'); 1000 return start; 1001} 1002 1003// ===== Storage history 5 let, týdennà data ===== 1004// Pattern: každý rok klesá v lednu-bÅeznu, narůstá duben-listopad, vrchol kolem listopadu 1005// Anchor: 11/2022 ~95% (po Ukraine war intensive filling), 04/2023 ~60%, 11/2023 ~99% (rekord), 04/2024 ~59%, 1006// 11/2024 ~89%, 04/2025 ~25%, 11/2025 ~93%, 05/2026 ~34% 1007 1008function generateStorageHistory() { 1009 // Týdny 1-52 pro každý rok (storage rok = duben-bÅezen, ale tady kalendáÅnÃ) 1010 const years = {}; 1011 // [datumStart, hodnota%] anchor body 1012 const anchors = { 1013 '2021': [ 1014 ['2021-01-01', 73], ['2021-02-01', 60], ['2021-03-01', 45], ['2021-04-01', 33], 1015 ['2021-05-01', 38], ['2021-06-01', 48], ['2021-07-01', 58], ['2021-08-01', 68], 1016 ['2021-09-01', 75], ['2021-10-01', 80], ['2021-11-01', 77], ['2021-12-01', 62] 1017 ], 1018 '2022': [ 1019 ['2022-01-01', 35], ['2022-03-01', 22], ['2022-04-01', 25], ['2022-05-01', 35], 1020 ['2022-06-01', 52], ['2022-07-01', 68], ['2022-08-01', 82], ['2022-09-01', 92], 1021 ['2022-10-01', 96], ['2022-11-01', 95], ['2022-12-01', 88] 1022 ], 1023 '2023': [ 1024 ['2023-01-01', 80], ['2023-02-01', 72], ['2023-03-01', 65], ['2023-04-01', 60], 1025 ['2023-05-01', 65], ['2023-06-01', 72], ['2023-07-01', 88], ['2023-08-01', 96], 1026 ['2023-09-01', 99], ['2023-10-01', 99], ['2023-11-01', 99], ['2023-12-01', 90] 1027 ], 1028 '2024': [ 1029 ['2024-01-01', 82], ['2024-02-01', 72], ['2024-03-01', 62], ['2024-04-01', 59], 1030 ['2024-05-01', 62], ['2024-06-01', 70], ['2024-07-01', 82], ['2024-08-01', 90], 1031 ['2024-09-01', 94], ['2024-10-01', 92], ['2024-11-01', 89], ['2024-12-01', 78] 1032 ], 1033 '2025': [ 1034 ['2025-01-01', 65], ['2025-02-01', 50], ['2025-03-01', 35], ['2025-04-01', 25], 1035 ['2025-05-01', 30], ['2025-06-01', 42], ['2025-07-01', 58], ['2025-08-01', 75], 1036 ['2025-09-01', 88], ['2025-10-01', 94], ['2025-11-01', 93], ['2025-12-01', 80] 1037 ], 1038 '2026': [
1039 ['2026-01-01', 68], ['2026-02-01', 50], ['2026-03-01', 38], ['2026-04-01', 32], 1040 ['2026-05-13', 33], ['2026-06-01', 37] 1041 ] 1042 }; 1043 1044 function interp(arr, dateStr) { 1045 const t = new Date(dateStr).getTime(); 1046 if (t <= new Date(arr[0][0]).getTime()) return arr[0][1]; 1047 if (t >= new Date(arr[arr.length-1][0]).getTime()) return arr[arr.length-1][1]; 1048 for (let i = 0; i < arr.length - 1; i++) { 1049 const t1 = new Date(arr[i][0]).getTime(); 1050 const t2 = new Date(arr[i+1][0]).getTime(); 1051 if (t >= t1 && t <= t2) { 1052 const ratio = (t - t1) / (t2 - t1); 1053 return arr[i][1] + ratio * (arr[i+1][1] - arr[i][1]); 1054 } 1055 } 1056 return arr[arr.length-1][1]; 1057 } 1058 1059 Object.keys(anchors).forEach(year => { 1060 const weekly = []; 1061 for (let w = 1; w <= 52; w++) { 1062 // ISO week to date 1063 const jan4 = new Date(parseInt(year), 0, 4); 1064 const weekStart = new Date(jan4.getTime() + (w-1) * 7 * 24*60*60*1000); 1065 if (year === '2026' && weekStart > TODAY) break; 1066 const ds = weekStart.toISOString().slice(0,10); 1067 const val = interp(anchors[year], ds); 1068 weekly.push({ week: w, value: Math.round(val*10)/10 }); 1069 } 1070 years[year] = weekly; 1071 }); 1072 return years; 1073} 1074 1075let storageHistory = generateStorageHistory(); // fallback â pÅepÃÅ¡e se reálnými AGSI+ daty v loadFromSupabase() 1076 1077// ===== Update latest price tables (sekce 1 + 2) ===== 1078// Naplnà Åádky tabulky aktuálnÃmi hodnotami z HISTORICAL_DATA a obarvà dennà zmÄnu 1079// (+ Äervená = růst, â zelená = pokles) podle smÄru. 1080function fmtNum(n, dec) { return n.toFixed(dec).replace('.', ','); } 1081function fmtCZ(n) { return Math.round(n).toLocaleString('cs-CZ'); } 1082function updateLatestPriceTables() { 1083 document.querySelectorAll('[data-series]').forEach(row => { 1084 if (row.tagName !== 'TR') return; 1085 const sid = row.dataset.series; 1086 const series = HISTORICAL_DATA?.series?.[sid]; 1087 if (!series || !series.points || series.points.length === 0) return; 1088 const pts = series.points; 1089 const last = pts[pts.length - 1]; 1090 const prev = pts.length > 1 ? pts[pts.length - 2] : null; 1091 const cells = row.querySelectorAll('td.num'); 1092 if (cells.length < 3) return; 1093 cells[0].textContent = fmtNum(last.value, 2); 1094 // CZK kurzem platným v den poslednà ceny (ÄNB fixing z fx_rates, fallback poslednà známý) 1095 const rate = fxOnDate(last.date) || fxLatest(); 1096 cells[1].textContent = rate ? fmtCZ(last.value * rate) : 'â'; 1097 // Pill jde do <span> uvnitÅ buÅky â td samotná zůstane table-cell s vertical-align: middle. 1098 cells[2].classList.remove('delta-up', 'delta-down', 'muted'); 1099 if (prev) { 1100 const diff = last.value - prev.value; 1101 const pct = (diff / prev.value) * 100; 1102 const arrow = diff > 0 ? 'â²' : (diff < 0 ? 'â¼' : '·'); 1103 const sign = diff > 0 ? '+' : (diff < 0 ? 'â' : ''); 1104 const absDiff = Math.abs(diff), absPct = Math.abs(pct); 1105 const cls = diff > 0 ? 'delta-up' : (diff < 0 ? 'delta-down' : 'muted'); 1106 const text = `${arrow} ${sign}${fmtNum(absDiff, 2)} (${sign}${fmtNum(absPct, 2)} %)`; 1107 cells[2].innerHTML = `<span class="${cls}">${text}</span>`; 1108 } else { 1109 cells[2].innerHTML = '<span class="muted">â</span>'; 1110 } 1111 // Datum platnosti hodnoty (obchodnà den poslednÃho settlementu dané série) 1112 if (cells[3]) cells[3].textContent = fmtDateCZ(last.date); 1113 }); 1114 // HlaviÄka aktuálnÃch cen â velké ÄÃslo + datum settlementu pod nÃm 1115 function fmtDateCZ(isoDate) { 1116 if (!isoDate) return 'â'; 1117 const [y, m, d] = isoDate.split('-'); 1118 return `${parseInt(d, 10)}. ${parseInt(m, 10)}. ${y}`; 1119 } 1120 function colorizeBigValue(elemId, pts) { 1121 if (!pts.length) return; 1122 const last = pts[pts.length-1]; 1123 const prev = pts.length > 1 ? pts[pts.length-2] : null; 1124 const elCur = document.getElementById(elemId); 1125 if (!elCur) return; 1126 elCur.classList.remove('delta-up', 'delta-down'); 1127 if (prev) { 1128 const diff = last.value - prev.value; 1129 if (diff > 0) elCur.classList.add('delta-up'); 1130 else if (diff < 0) elCur.classList.add('delta-down'); 1131 } 1132 // Inline mini-badge se Å¡ipkou + dennà zmÄnou 1133 let html = `${fmtNum(last.value, 2)} EUR/MWh`; 1134 if (prev) { 1135 const diff = last.value - prev.value; 1136 const pct = (diff / prev.value) * 100; 1137 const arrow = diff > 0 ? 'â²' : (diff < 0 ? 'â¼' : '·'); 1138 const sign = diff > 0 ? '+' : (diff < 0 ? 'â' : ''); 1139 const cls = diff > 0 ? 'delta-up' : (diff < 0 ? 'delta-down' : 'muted'); 1140 html += ` <span class="${cls}" style="font-size:.5em;vertical-align:middle;margin-left:8px;font-weight:700">${arrow} ${sign}${fmtNum(Math.abs(diff), 2)} (${sign}${fmtNum(Math.abs(pct), 2)} %)</span>`;
1141 } 1142 elCur.innerHTML = html; 1143 } 1144 const el27Pts = HISTORICAL_DATA?.series?.el27?.points || []; 1145 colorizeBigValue('el-current', el27Pts); 1146 if (el27Pts.length) { 1147 const last = el27Pts[el27Pts.length-1]; 1148 const elDate = document.getElementById('el-current-date'); 1149 if (elDate) elDate.textContent = fmtDateCZ(last.date); 1150 } 1151 const gas27Pts = HISTORICAL_DATA?.series?.gas27?.points || []; 1152 colorizeBigValue('gas-current', gas27Pts); 1153 if (gas27Pts.length) { 1154 const last = gas27Pts[gas27Pts.length-1]; 1155 const gasDate = document.getElementById('gas-current-date'); 1156 if (gasDate) gasDate.textContent = fmtDateCZ(last.date); 1157 } 1158 const euaPts = HISTORICAL_DATA?.series?.eua?.points || []; 1159 if (euaPts.length) { 1160 const l = euaPts[euaPts.length-1], p = euaPts.length>1?euaPts[euaPts.length-2]:null; 1161 const elc = document.getElementById('eua-current'); 1162 if (elc) { 1163 elc.classList.remove('delta-up','delta-down'); 1164 let html = `${fmtNum(l.value,2)} EUR/t`; 1165 if (p) { const diff=l.value-p.value, pct=diff/p.value*100, up=diff>0; const arrow=up?'\u25B2':(diff<0?'\u25BC':'\u00B7'), sign=up?'+':(diff<0?'\u2212':''), cls=up?'delta-up':(diff<0?'delta-down':'muted'); if(up)elc.classList.add('delta-up'); else if(diff<0)elc.classList.add('delta-down'); html += ` <span class="${cls}" style="font-size:.5em;vertical-align:middle;margin-left:8px;font-weight:700">${arrow} ${sign}${fmtNum(Math.abs(diff),2)} (${sign}${fmtNum(Math.abs(pct),2)} %)</span>`; } 1166 elc.innerHTML = html; 1167 } 1168 const dEl = document.getElementById('eua-current-date'); if (dEl) dEl.textContent = fmtDateCZ(l.date); 1169 } 1170 // Ropa â velké ÄÃslo (Brent) + tabulka; jednotka dle pÅepÃnaÄe (USD/bbl · USD/t · USD/GJ · KÄ/l · KÄ/bbl) 1171 const brentPts = HISTORICAL_DATA?.series?.brent?.points || []; 1172 if (brentPts.length) { 1173 const l = brentPts[brentPts.length-1], p = brentPts.length>1?brentPts[brentPts.length-2]:null; 1174 const lv = oilConv(l.value, 'brent', l.date), pv = p ? oilConv(p.value, 'brent', p.date) : null; 1175 const oc = document.getElementById('oil-current'); 1176 if (oc) { 1177 oc.classList.remove('delta-up','delta-down'); 1178 let html = `${oilFmt(lv)} ${oilUnitLabel()}`; 1179 if (pv != null) { const diff=lv-pv, pct=diff/pv*100, up=diff>0; const arrow=up?'\u25B2':(diff<0?'\u25BC':'\u00B7'), sign=up?'+':(diff<0?'\u2212':''), cls=up?'delta-up':(diff<0?'delta-down':'muted'); if(up)oc.classList.add('delta-up'); else if(diff<0)oc.classList.add('delta-down'); html += ` <span class="${cls}" style="font-size:.5em;vertical-align:middle;margin-left:8px;font-weight:700">${arrow} ${sign}${oilFmt(Math.abs(diff))} (${sign}${fmtNum(Math.abs(pct),2)} %)</span>`; } 1180 oc.innerHTML = html; 1181 } 1182 const dO = document.getElementById('oil-current-date'); if (dO) dO.textContent = fmtDateCZ(l.date); 1183 const th = document.getElementById('oil-price-header'); if (th) th.textContent = `Cena (${oilUnitLabel()})`; 1184 } 1185 document.querySelectorAll('tr[data-oil]').forEach(row => { 1186 const sid = row.dataset.oil; 1187 const pts = HISTORICAL_DATA?.series?.[sid]?.points || []; 1188 if (!pts.length) return; 1189 const last = pts[pts.length-1], prev = pts.length>1?pts[pts.length-2]:null; 1190 const lv = oilConv(last.value, sid, last.date), pv = prev ? oilConv(prev.value, sid, prev.date) : null; 1191 const cells = row.querySelectorAll('td.num'); 1192 if (cells.length < 2) return; 1193 cells[0].textContent = oilFmt(lv); 1194 cells[1].classList.remove('delta-up','delta-down','muted'); 1195 if (pv != null) { const diff=lv-pv, pct=diff/pv*100; const arrow=diff>0?'\u25B2':(diff<0?'\u25BC':'\u00B7'), sign=diff>0?'+':(diff<0?'\u2212':''), cls=diff>0?'delta-up':(diff<0?'delta-down':'muted'); cells[1].innerHTML=`<span class="${cls}">${arrow} ${sign}${oilFmt(Math.abs(diff))} (${sign}${fmtNum(Math.abs(pct),2)} %)</span>`; } 1196 else cells[1].innerHTML='<span class="muted">\u2014</span>'; 1197 if (cells[2]) cells[2].textContent = fmtDateCZ(last.date); 1198 }); 1199} 1200 1201// ===== Legenda: skryté Åady NEÅ KRTAT, jen ztlumit průhlednostà â platà pro VÅ ECHNY grafy ===== 1202(function () { 1203 if (typeof Chart === 'undefined' || !Chart.defaults) return; 1204 function fadeColor(c, a) { 1205 if (typeof c !== 'string') return c; 1206 if (a == null) a = 0.28; 1207 var m = c.match(/^rgba?\(([^)]+)\)$/i); 1208 if (m) { 1209 var p = m[1].split(','); 1210 return 'rgba(' + p[0].trim() + ',' + p[1].trim() + ',' + p[2].trim() + ',' + a + ')'; 1211 } 1212 if (c.charAt(0) === '#') { 1213 var h = c.slice(1); 1214 if (h.length === 3) h = h.split('').map(function (x) { return x + x; }).join(''); 1215 if (h.length === 8) h = h.slice(0, 6); 1216 var r = parseInt(h.slice(0, 2), 16), g = parseInt(h.slice(2, 4), 16), b = parseInt(h.slice(4, 6), 16); 1217 if (isNaN(r) || isNaN(g) || isNaN(b)) return c; 1218 return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; 1219 } 1220 return c; 1221 } 1222 var baseGen = Chart.defaults.plugins.legend.labels.generateLabels; 1223 Chart.defaults.plugins.legend.labels.generateLabels = function (chart) { 1224 var items = baseGen.call(this, chart);
1225 items.forEach(function (it) { 1226 var visible = true; 1227 try { 1228 if (it.datasetIndex != null) visible = chart.isDatasetVisible(it.datasetIndex); 1229 else if (it.index != null && typeof chart.getDataVisibility === 'function') visible = chart.getDataVisibility(it.index); 1230 } catch (e) {} 1231 if (!visible) { 1232 it.hidden = false; // zrušà pÅeÅ¡krtnutà textu 1233 it.fontColor = 'rgba(140,140,140,0.5)'; // ztlumený text 1234 it.fillStyle = fadeColor(it.fillStyle, 0.22); 1235 it.strokeStyle = fadeColor(it.strokeStyle, 0.35); 1236 } 1237 }); 1238 return items; 1239 }; 1240})(); 1241 1242// ===== Glow efekt pro âNext year" Åady (jako glow v Excelu) ===== 1243// Stejná tloušťka Äáry jako ostatnà Åady, ale s mÄkkou záÅà v barvÄ Åady. 1244const nextYearGlowPlugin = { 1245 id: 'nextYearGlow', 1246 beforeDatasetDraw: function (chart, args) { 1247 const ds = chart.data.datasets[args.index]; 1248 if (ds && /Next year/i.test(ds.label || '') && chart.isDatasetVisible(args.index)) { 1249 const ctx = chart.ctx; 1250 ctx.save(); 1251 ctx.shadowColor = ds.borderColor; 1252 ctx.shadowBlur = 10; 1253 } 1254 }, 1255 afterDatasetDraw: function (chart, args) { 1256 const ds = chart.data.datasets[args.index]; 1257 if (ds && /Next year/i.test(ds.label || '') && chart.isDatasetVisible(args.index)) { 1258 chart.ctx.restore(); 1259 } 1260 } 1261}; 1262 1263// ===== Charts ===== 1264// priceCharts: { el: Chart, gas: Chart } â dva nezávislé grafy 3a (elektÅina) + 3b (plyn) 1265const priceCharts = {}; 1266// DefaultnÄ zobrazen v graf 3A jen el27, v 3B jen gas27 â ostatnà jsou skryté (ztlumené v legendÄ), 1267// zapnout je lze kliknutÃm na ztlumenou položku v legendÄ Chart.js. 1268// yZero=false (default) = Y-osa adaptivnÃ; true = vždy zaÄÃná od 0. 1269const priceChartState = { 1270 // Default: zobrazen jen Cal-27, obdobà poslednà mÄsÃc (1m). Ostatnà Åady skryté (lze zapnout v legendÄ). 1271 el: { range: '1m', yZero: false, hidden: { el_nextyear:false, el26:true, el28:true, el29:true, gas_nextyear:true, gasTTF:true, gas27:true, gas28:true, gas29:true } }, 1272 gas: { range: '1m', yZero: false, hidden: { el_nextyear:true, el26:true, el27:true, el28:true, el29:true, gas_nextyear:false, gasTTF:true, gas28:true, gas29:true } } 1273}; 1274let priceChart, storageHistoryChart; // legacy reference (kept for backwards compat) 1275 1276// ===== Supabase async data loader ===== 1277/* ââ PÅihlášenà ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1278 Brána nesloužà jen ke skrytà stránky: bez platné session a bez záznamu 1279 v cz_pristup_energoreport nevrátà databáze žádná data (RLS), takže ani 1280 obejità téhle obrazovky nic neodhalÃ. */ 1281function supaClient() { 1282 if (!window.__SUPA && typeof supabase !== 'undefined' && supabase.createClient) { 1283 window.__SUPA = supabase.createClient(window.__SUPABASE_URL, window.__SUPABASE_ANON, 1284 // PKCE: po návratu z Microsoftu pÅijde ?code=⦠mÃsto tokenu v adrese; 1285 // supabase-js si výmÄnu za session udÄlá sám (detectSessionInUrl). 1286 { auth: { flowType: 'pkce', detectSessionInUrl: true, persistSession: true, autoRefreshToken: true } }); 1287 } 1288 return window.__SUPA; 1289} 1290 1291async function authStav() { // 'ok' | 'bezpristupu' | 'neprihlasen' 1292 const s = supaClient(); if (!s) return 'neprihlasen'; 1293 let session = null; 1294 try { session = (await s.auth.getSession()).data.session; } catch (e) { return 'neprihlasen'; } 1295 if (!session) return 'neprihlasen'; 1296 try { 1297 const { data, error } = await s.rpc('je_clen_energoreport'); 1298 return (!error && data === true) ? 'ok' : 'bezpristupu'; 1299 } catch (e) { return 'bezpristupu'; } 1300} 1301 1302function authUkazBranu(hlaska) { 1303 const ov = document.getElementById('supa-loading'); if (ov) ov.classList.add('hidden'); 1304 const g = document.getElementById('auth-gate'); if (!g) return; 1305 g.hidden = false; 1306 if (hlaska) { const e = document.getElementById('auth-err'); if (e) { e.textContent = hlaska; e.hidden = false; } } 1307 const f = document.getElementById('auth-form'); 1308 if (f && !f.dataset.bound) { 1309 f.dataset.bound = '1'; 1310 f.addEventListener('submit', async (ev) => { 1311 ev.preventDefault(); 1312 const btn = document.getElementById('auth-submit'); 1313 const err = document.getElementById('auth-err'); 1314 const email = document.getElementById('auth-email').value.trim(); 1315 const heslo = document.getElementById('auth-pass').value; 1316 btn.disabled = true; btn.textContent = 'PÅihlaÅ¡ujiâ¦'; if (err) err.hidden = true; 1317 const s = supaClient(); 1318 const { error } = await s.auth.signInWithPassword({ email, password: heslo }); 1319 if (error) { 1320 if (err) { err.textContent = 'PÅihlášenà se nezdaÅilo â zkontrolujte e-mail a heslo.'; err.hidden = false; } 1321 btn.disabled = false; btn.textContent = 'PÅihlásit se'; 1322 return; 1323 } 1324 // pÅihlášenà proÅ¡lo, ale rozhoduje whitelist 1325 const { data: clen } = await s.rpc('je_clen_energoreport'); 1326 if (clen !== true) { 1327 await s.auth.signOut(); 1328 if (err) { err.textContent = 'ÃÄet nemá k tomuto reportu pÅÃstup. Požádejte správce.'; err.hidden = false; } 1329 btn.disabled = false; btn.textContent = 'PÅihlásit se'; 1330 return; 1331 } 1332 location.reload(); 1333 }); 1334 } 1335} 1336 1337async function authMicrosoft() { 1338 const s = supaClient(); if (!s) return; 1339 const err = document.getElementById('auth-err'); 1340 if (err) err.hidden = true; 1341 // Stejné nastavenà jako pÅihlášenà do aplikace Energetika (provider azure). 1342 // Statická stránka nemá serverovou /auth/callback â supabase-js si výmÄnu kódu 1343 // za session udÄlá sám pÅi naÄtenà (detectSessionInUrl), takže se vracÃme sem. 1344 const { error } = await s.auth.signInWithOAuth({ 1345 provider: 'azure', 1346 options: { redirectTo: window.location.origin + window.location.pathname, scopes: 'openid email profile' } 1347 }); 1348 if (error && err) { 1349 err.textContent = 'PÅihlášenà pÅes Microsoft selhalo: ' + error.message; 1350 err.hidden = false;
1351 } 1352} 1353 1354async function authOdhlasit() { 1355 try { await supaClient().auth.signOut(); } catch (e) {} 1356 try { indexedDB.deleteDatabase('erpcache'); } catch (e) {} // aÅ¥ dalšà uživatel nevidà data z cache 1357 location.reload(); 1358} 1359 1360async function authHlavicka() { 1361 const s = supaClient(); if (!s) return; 1362 let email = ''; 1363 try { email = (await s.auth.getUser()).data.user?.email || ''; } catch (e) {} 1364 const box = document.querySelector('.header-actions'); if (!box) return; 1365 const el = document.createElement('div'); 1366 el.className = 'auth-who'; 1367 el.innerHTML = '<span title="PÅihlášen">' + email.replace(/</g, '<') + '</span>' 1368 + '<span>·</span><button type="button" onclick="authOdhlasit()">Odhlásit</button>'; 1369 box.appendChild(el); 1370} 1371 1372async function loadFromSupabase() { 1373 if (typeof supabase === 'undefined' || !supabase.createClient) { 1374 console.warn('supabase-js not loaded â pokraÄuji s prázdnými daty'); 1375 return false; 1376 } 1377 if (!window.__SUPA) { 1378 window.__SUPA = supabase.createClient(window.__SUPABASE_URL, window.__SUPABASE_ANON, 1379 // PKCE: po návratu z Microsoftu pÅijde ?code=⦠mÃsto tokenu v adrese; 1380 // supabase-js si výmÄnu za session udÄlá sám (detectSessionInUrl). 1381 { auth: { flowType: 'pkce', detectSessionInUrl: true, persistSession: true, autoRefreshToken: true } }); 1382 } 1383 const supa = window.__SUPA; 1384 const setMsg = (m) => { const el = document.getElementById('supa-loading-msg'); if (el) el.textContent = m; }; 1385 1386 // PostgREST/Supabase vracà max 1000 Åádků na požadavek. Tahle pomocná funkce 1387 // si nejdÅÃv zjistà poÄet Åádků a pak natáhne VÅ ECHNY stránky (paralelnÄ). 1388 // RPC má stejný strop 1000 Åádků jako bÄžný select â bez stránkovánà by se data 1389 // TIÅ E uÅÃzla (spot_daily: 1000 z 2430). Tady je offset neÅ¡kodný, jde o pár stránek. 1390 async function fetchAllRpc(fn) { 1391 const PAGE = 1000; 1392 let out = [], off = 0; 1393 for (let i = 0; i < 50; i++) { 1394 const r = await supa.rpc(fn).range(off, off + PAGE - 1); 1395 if (r.error) { console.error('rpc ' + fn, r.error); break; } 1396 const d = r.data || []; 1397 out = out.concat(d); 1398 if (d.length < PAGE) break; 1399 off += PAGE; 1400 } 1401 return out; 1402 } 1403 1404 async function fetchAllRows(table, columns, orderCol) { 1405 const ckey = 'erp:v2:' + table + ':' + (columns || '*'); 1406 try { const o = await Promise.race([__idbCache.get(ckey), new Promise(res => setTimeout(() => res(null), 1500))]); if (o && (Date.now() - o.t) < 1800000) return o.d; } catch (e) {} 1407 const PAGE = 1000; 1408 const krajniHodnota = async (vzestupne) => { 1409 if (!orderCol) return null; 1410 const r = await supa.from(table).select(orderCol).order(orderCol, { ascending: vzestupne }).limit(1); 1411 return (!r.error && r.data && r.data.length) ? r.data[0][orderCol] : null; 1412 }; 1413 // count i krajnà hodnoty najednou â jinak by to byla dvÄ kola latence za sebou 1414 const [{ count, error: ce }, krajMin, krajMax] = await Promise.all([ 1415 supa.from(table).select('*', { count: 'exact', head: true }), 1416 krajniHodnota(true), krajniHodnota(false) 1417 ]); 1418 if (ce) { console.error('count ' + table, ce); } 1419 const total = count || 0; 1420 1421 // Bez ÅadicÃho sloupce nebo do jedné stránky â jeden prostý dotaz. 1422 if (!orderCol || total <= PAGE) { 1423 let q = supa.from(table).select(columns); 1424 if (orderCol) q = q.order(orderCol); 1425 const r = await q.limit(PAGE); 1426 if (r.error) { console.error('fetchAll ' + table, r.error); return []; } 1427 const d = r.data || []; 1428 __idbCache.set(ckey, { t: Date.now(), d: d }); 1429 return d; 1430 } 1431 1432 // POZOR na OFFSET: PostgREST musà u N-té stránky pÅeskákat NÃ1000 Åádků, takže 1433 // náklady rostou kvadraticky (u spot_hourly ~1,7 mil. navÅ¡tÃvených Åádků na jedno 1434 // naÄtenÃ, poslednà stránka 465 ms). MÃsto toho dÄlÃme rozsah PODLE HODNOTY klÃÄe 1435 // â každý dotaz je pak rozsahové Ätenà indexu (~13 ms) a jdou dál paralelnÄ. 1436 const minV = krajMin, maxV = krajMax; 1437 if (minV == null || maxV == null) return []; 1438 1439 // KlÃÄ je datum ('YYYY-MM-DD') nebo Äasové razÃtko â hranice se musà posÃlat ve 1440 // STEJNÃM formátu, jinak by je Postgres nepÅetypoval. 1441 const jenDatum = /^\d{4}-\d{2}-\d{2}$/.test(String(minV)); 1442 const naMs = (v) => new Date(jenDatum ? String(v) + 'T00:00:00Z' : v).getTime(); 1443 const naHodnotu = (ms) => jenDatum ? new Date(ms).toISOString().slice(0, 10) : new Date(ms).toISOString();
1444 const krok = jenDatum ? 86400000 : 1000; // nejmenšà dÄlitelný krok 1445 1446 const odMs = naMs(minV), doMs = naMs(maxV); 1447 if (!isFinite(odMs) || !isFinite(doMs) || doMs < odMs) return []; 1448 1449 // NaÄti úsek [a, b); poslednà úsek vÄetnÄ hornà hranice. Když se vrátà pÅesnÄ PAGE 1450 // Åádků, mohl být oÅÃznutý â rozpůlÃme a naÄteme znovu (rekurzivnÄ). 1451 const usek = async (aMs, bMs, posledni, hloubka) => { 1452 let q = supa.from(table).select(columns).order(orderCol).gte(orderCol, naHodnotu(aMs)); 1453 q = posledni ? q.lte(orderCol, naHodnotu(bMs)) : q.lt(orderCol, naHodnotu(bMs)); 1454 const r = await q.limit(PAGE); 1455 if (r.error) { console.error('fetchAll ' + table, r.error); return []; } 1456 const d = r.data || []; 1457 if (d.length < PAGE) return d; 1458 if (bMs - aMs > krok && hloubka < 24) { // možná oÅÃznuto â rozpůlit 1459 const stred = aMs + Math.floor((bMs - aMs) / 2 / krok) * krok; 1460 if (stred > aMs && stred < bMs) { 1461 const [l, p] = await Promise.all([usek(aMs, stred, false, hloubka + 1), usek(stred, bMs, posledni, hloubka + 1)]); 1462 return l.concat(p); 1463 } 1464 } 1465 // Dál dÄlit nelze (jedna hodnota klÃÄe s â¥1000 Åádky) â dobrat zbytek pÅes offset. 1466 console.warn('fetchAll ' + table + ': úsek nelze dÄlit, dobÃrám offsetem'); 1467 let zbytek = [], off = PAGE; 1468 for (let i = 0; i < 20; i++) { 1469 let q2 = supa.from(table).select(columns).order(orderCol).gte(orderCol, naHodnotu(aMs)); 1470 q2 = posledni ? q2.lte(orderCol, naHodnotu(bMs)) : q2.lt(orderCol, naHodnotu(bMs)); 1471 const r2 = await q2.range(off, off + PAGE - 1); 1472 if (r2.error || !r2.data || !r2.data.length) break; 1473 zbytek = zbytek.concat(r2.data); 1474 if (r2.data.length < PAGE) break; 1475 off += PAGE; 1476 } 1477 return d.concat(zbytek); 1478 }; 1479 1480 // Ãseky mÃÅà na ~60 % limitu, ne na 100 %. Kdyby cÃlily pÅesnÄ na 1000 Åádků, 1481 // každá nerovnomÄrnost v datech by strop pÅekroÄila a vyvolala dalšà kolo dÄlenà 1482 // (a tÃm dalšà round-trip). RadÅ¡i vÃc menÅ¡Ãch dotazů â jdou stejnÄ paralelnÄ. 1483 const pocet = Math.max(1, Math.ceil(total / (PAGE * 0.6))); 1484 const sirka = Math.max(krok, Math.ceil((doMs - odMs + krok) / pocet / krok) * krok); 1485 const ukoly = []; 1486 for (let a = odMs; a <= doMs; a += sirka) { 1487 const b = Math.min(a + sirka, doMs); 1488 ukoly.push(usek(a, b, a + sirka > doMs, 0)); 1489 } 1490 const casti = await Promise.all(ukoly); 1491 let out = []; 1492 for (const c of casti) out = out.concat(c); 1493 1494 if (total && out.length !== total) { 1495 console.warn('fetchAll ' + table + ': naÄteno ' + out.length + ' z ' + total + ' Åádků'); 1496 } 1497 __idbCache.set(ckey, { t: Date.now(), d: out }); 1498 return out; 1499 } 1500 1501 const PG = location.pathname.includes('elektrina') ? 'el' : location.pathname.includes('teplo') ? 'teplo' : location.pathname.includes('plyn') ? 'plyn' : 'main'; 1502 setMsg('NaÄÃtám PXE futures cenyâ¦'); 1503 const prices = await fetchAllRows('price_series', 'series_id, trading_date, value', 'trading_date'); 1504 prices.forEach(r => { 1505 const s = HISTORICAL_DATA.series[r.series_id]; 1506 if (s) s.points.push({ date: r.trading_date, value: parseFloat(r.value) }); 1507 }); 1508 // âNext year" série: pokud jsou v DB nahraná historická data (PXE/kurzy.cz xlsx, 1509 // pÅepoÄtená na EUR/MWh dennÃm kurzem ECB), použij je a doplÅ novÄjšà dny dopoÄtem 1510 // ze stitched Cal-X kontraktů; jinak použij jen dopoÄet. 1511 ['el', 'gas'].forEach(p => { 1512 const s = HISTORICAL_DATA.series[p + '_nextyear']; 1513 if (!s) return; 1514 const uploaded = s.points.slice().sort((a, b) => a.date < b.date ? -1 : (a.date > b.date ? 1 : 0)); 1515 const computed = computeNextYearSeries(p); 1516 if (uploaded.length) { 1517 const lastUp = uploaded[uploaded.length - 1].date; 1518 s.points = uploaded.concat(computed.filter(pt => pt.date > lastUp)); 1519 } else { 1520 s.points = computed; 1521 } 1522 }); 1523 1524 setMsg('NaÄÃtám kurzy EUR/CZKâ¦'); 1525 try { 1526 // ÄNB dennà fixing z fx_rates (od 2020, plnà dennà task). PÅekryje statický 1527 // ECB seznam FX_DAILY a prodloužà ho do dneÅ¡ka (forward-fill vÃkendů/svátků). 1528 const fx = await fetchAllRows('fx_rates', 'rate_date, eur_czk, usd_czk', 'rate_date'); 1529 if (fx && fx.length) { 1530 const a = window.FX_DAILY, base = window.FX_BASE_MS; 1531 const idxOf = ds => Math.floor((Date.UTC(+ds.slice(0,4), +ds.slice(5,7)-1, +ds.slice(8,10)) - base) / 86400000); 1532 const byIdx = {}; 1533 fx.forEach(r =>
1533 { byIdx[idxOf(r.rate_date)] = parseFloat(r.eur_czk); }); 1534 const firstIdx = idxOf(fx[0].rate_date); 1535 const todayIdx = Math.floor((Date.now() - base) / 86400000); 1536 let lastVal = a[Math.min(firstIdx, a.length) - 1] || a[a.length - 1]; 1537 for (let i = firstIdx; i <= todayIdx; i++) { 1538 if (byIdx[i] !== undefined) lastVal = byIdx[i]; 1539 a[i] = lastVal; 1540 } 1541 const sums = {}; 1542 fx.forEach(r => { const y = r.rate_date.slice(0,4); (sums[y] = sums[y] || []).push(parseFloat(r.eur_czk)); }); 1543 Object.keys(sums).forEach(y => { window.FX_YEAR_AVG[y] = Math.round(sums[y].reduce((s,v)=>s+v,0) / sums[y].length * 1000) / 1000; }); 1544 // USD/CZK (od 2020; pro ropu 2022+ staÄÃ) â vlastnà bázový index 1545 const usdRows = fx.filter(r => r.usd_czk != null); 1546 if (usdRows.length) { 1547 const ubase = Date.UTC(+usdRows[0].rate_date.slice(0,4), +usdRows[0].rate_date.slice(5,7)-1, +usdRows[0].rate_date.slice(8,10)); 1548 window.FX_USD_BASE_MS = ubase; 1549 const uidx = ds => Math.floor((Date.UTC(+ds.slice(0,4), +ds.slice(5,7)-1, +ds.slice(8,10)) - ubase) / 86400000); 1550 const uByIdx = {}; usdRows.forEach(r => { uByIdx[uidx(r.rate_date)] = parseFloat(r.usd_czk); }); 1551 const utoday = Math.floor((Date.now() - ubase) / 86400000); 1552 const uarr = []; let ulast = parseFloat(usdRows[0].usd_czk); 1553 for (let i = 0; i <= utoday; i++) { if (uByIdx[i] !== undefined) ulast = uByIdx[i]; uarr[i] = ulast; } 1554 window.FX_USD_DAILY = uarr; 1555 } 1556 window.__fxLatestDate = fx[fx.length-1].rate_date; 1557 } 1558 } catch (e) { console.error('fx_rates load failed', e); } 1559 1560 setMsg('NaÄÃtám OTE 15min profilyâ¦'); 1561 const ote15 = PG === 'main' ? await fetchAllRows('spot_15min', 'trading_date, slot_index, price_eur_mwh', 'trading_date') : []; 1562 if (ote15) {
1563 ote15.forEach(r => { 1564 if (!window.ote15Data[r.trading_date]) window.ote15Data[r.trading_date] = new Array(96).fill(null); 1565 window.ote15Data[r.trading_date][r.slot_index] = parseFloat(r.price_eur_mwh); 1566 }); 1567 } 1568 1569 setMsg('NaÄÃtám hodinové spot ceny 2020-2026 (56k bodů)â¦'); 1570 const all = PG === 'main' ? await fetchAllRows('spot_hourly', 'ts, price_eur_mwh', 'ts') : []; 1571 window.spotData.prices = all.map(r => parseFloat(r.price_eur_mwh)); 1572 window.spotData.totalHours = all.length; 1573 if (all.length) { 1574 window.spotData.firstTs = Math.floor(new Date(all[0].ts).getTime()/1000); 1575 window.spotData.firstDate = all[0].ts.slice(0,10) + ' ' + all[0].ts.slice(11,16); 1576 window.spotData.lastTs = Math.floor(new Date(all[all.length-1].ts).getTime()/1000); 1577 window.spotData.lastDate = all[all.length-1].ts.slice(0,10) + ' 23:00'; 1578 } 1579 1580 // ===== Kombinovaná vysokorozliÅ¡ovacà série pro graf 5 (reálné timestampy) ===== 1581 // Hodinová data + 15min tam, kde jsou dostupná (spot_15min). Pro dny s 15min daty 1582 // nahradÃme hodinové body Ätvrthodinovými, aby graf 5 ukazoval co nejvÄtšà detail. 1583 (function buildSpotSeries() { 1584 const fifteenDays = new Set(Object.keys(window.ote15Data || {})); 1585 const series = []; 1586 all.forEach(r => { 1587 const ms = new Date(r.ts).getTime(); 1588 const day = new Date(ms).toLocaleDateString('sv-SE'); // YYYY-MM-DD v lokálnÃm (CET) Äase 1589 if (fifteenDays.has(day)) return; // den má 15min â vynech hodinová 1590 const v = parseFloat(r.price_eur_mwh); 1591 if (isFinite(v)) series.push({ x: ms, y: v }); 1592 }); 1593 Object.keys(window.ote15Data || {}).forEach(day => { 1594 const arr = window.ote15Data[day]; 1595 if (!Array.isArray(arr)) return; 1596 const base = new Date(day + 'T00:00:00').getTime(); // lokálnà půlnoc (CET) 1597 arr.forEach((v, slot) => { 1598 if (v === null || v === undefined || !isFinite(v)) return; 1599 series.push({ x: base + slot * 15 * 60000, y: v }); 1600 }); 1601 }); 1602 series.sort((a, b) => a.x - b.x); 1603 window.spotData.series = series; 1604 })(); 1605 1606 setMsg('NaÄÃtám hodinové/DoW agregáty a dennà dataâ¦'); 1607 const [{ data: hby }, { data: dby }, dd] = await Promise.all([ 1608 supa.rpc('spot_hourly_by_year_read'), 1609 supa.rpc('spot_dow_by_year_read'), 1610 fetchAllRpc('spot_daily_read') // matview je zamÄený â fail-closed RPC, stránkovanÄ (strop 1000 platà i na RPC) 1611 ]); 1612 if (hby) { 1613 hby.forEach(r => { 1614 const y = String(r.year); 1615 if (!window.spotData.avgByHourByYear[y]) window.spotData.avgByHourByYear[y] = new Array(24).fill(0); 1616 window.spotData.avgByHourByYear[y][r.hour_local] = parseFloat(r.avg_price); 1617 }); 1618 const years = ['2023','2024','2025']; 1619 window.spotData.avgByHour3yr = new Array(24).fill(0).map((_, h) => { 1620 let s = 0, n = 0; 1621 years.forEach(y => { if (window.spotData.avgByHourByYear[y]) { s += window.spotData.avgByHourByYear[y][h]; n++; } }); 1622 return n ? s/n : 0; 1623 }); 1624 } 1625 if (dby) { 1626 dby.forEach(r => { 1627 const y = String(r.year); 1628 if (!window.spotData.avgByDowByYear[y]) window.spotData.avgByDowByYear[y] = new Array(7).fill(0); 1629 window.spotData.avgByDowByYear[y][r.dow] = parseFloat(r.avg_price); 1630 }); 1631 const years = ['2023','2024','2025']; 1632 window.spotData.avgByDow3yr = new Array(7).fill(0).map((_, d) => { 1633 let s = 0, n = 0; 1634 years.forEach(y => { if (window.spotData.avgByDowByYear[y]) { s += window.spotData.avgByDowByYear[y][d]; n++; } }); 1635 return n ? s/n : 0; 1636 }); 1637 } 1638 if (dd) { 1639 dd.forEach(r => { 1640 const d = new Date(r.trading_date); 1641 const y = String(d.getUTCFullYear()); 1642 if (!window.spotData.dailyByYear[y]) { 1643 const isLeap = (d.getUTCFullYear() % 4 === 0 && d.getUTCFullYear() % 100 !== 0) || d.getUTCFullYear() % 400 === 0; 1644 window.spotData.dailyByYear[y] = new Array(isLeap ? 366 : 365).fill(null); 1645 } 1646 const jan1 = Date.UTC(d.getUTCFullYear(), 0, 1); 1647 const doy = Math.floor((d.getTime() - jan1) / 86400000); 1648 window.spotData.dailyByYear[y][doy] = parseFloat(r.avg_price); 1649 }); 1650 } 1651 1652 setMsg('NaÄÃtám ENTSOE výrobu a bilanciâ¦'); 1653 const [gen, flow, units] = PG === 'el' ? await Promise.all([ 1654 fetchAllRows('entsoe_generation', '*', 'trading_date'), // 1600+ Åádků â paginovat 1655 fetchAllRows('entsoe_flows', '*', 'trading_date'), 1656 fetchAllRows('entsoe_unit_generation', '*', 'trading_date') // výroba po elektrárnách (A73) 1657 ]) : [[], [], []]; 1658 if (units) {
1659 units.forEach(r => { 1660 const d = r.trading_date; 1661 if (!window.entsoeUnitHistory[d]) window.entsoeUnitHistory[d] = {}; 1662 const uh = window.entsoeUnitHistory[d]; 1663 if (!uh[r.plant]) uh[r.plant] = new Array(24).fill(0); 1664 uh[r.plant][r.hour_local] = parseFloat(r.mw_avg); 1665 }); 1666 } 1667 if (gen) { 1668 gen.forEach(r => { 1669 const d = r.trading_date; 1670 if (!window.entsoeHistory[d]) window.entsoeHistory[d] = { generation_mw_hourly: {}, flows_mw_hourly: {} }; 1671 const gh = window.entsoeHistory[d].generation_mw_hourly; 1672 if (!gh[r.source_type]) gh[r.source_type] = new Array(24).fill(0); 1673 gh[r.source_type][r.hour_local] = parseFloat(r.mw_avg); 1674 }); 1675 } 1676 if (flow) { 1677 flow.forEach(r => { 1678 const d = r.trading_date; 1679 if (!window.entsoeHistory[d]) window.entsoeHistory[d] = { generation_mw_hourly: {}, flows_mw_hourly: {} }; 1680 const fh = window.entsoeHistory[d].flows_mw_hourly; 1681 if (!fh[r.neighbor]) fh[r.neighbor] = { export_to_cz_neighbor: new Array(24).fill(0), import_from_cz_neighbor: new Array(24).fill(0), net_export_cz: new Array(24).fill(0) }; 1682 fh[r.neighbor].export_to_cz_neighbor[r.hour_local] = parseFloat(r.export_mw); 1683 fh[r.neighbor].import_from_cz_neighbor[r.hour_local] = parseFloat(r.import_mw); 1684 fh[r.neighbor].net_export_cz[r.hour_local] = parseFloat(r.net_export_mw); 1685 }); 1686 } 1687 const days = Object.keys(window.entsoeHistory).sort(); 1688 if (days.length) { 1689 const latest = days[days.length - 1]; 1690 window.entsoeData = Object.assign({date: latest, timezone_note: 'CEST aligned (UTC+2)'}, window.entsoeHistory[latest]); 1691 } 1692 1693 // Ceny regulaÄnÃch záloh (FCR/aFRR/mFRR) â ENTSO-E A15, dennà agregát 1694 try { 1695 const bal = false ? await fetchAllRows('balancing_capacity', 'trading_date, reserve_type, direction, price_avg, price_max, reserve_mw', 'trading_date') : []; 1696 window.balancingData = {}; 1697 bal.forEach(r => { 1698 const k = r.reserve_type + '_' + r.direction; 1699 (window.balancingData[k] = window.balancingData[k] || []).push({ 1700 date: r.trading_date, 1701 avg: r.price_avg == null ? null : parseFloat(r.price_avg), 1702 max: r.price_max == null ? null : parseFloat(r.price_max), 1703 mw: r.reserve_mw == null ? null : parseFloat(r.reserve_mw) 1704 }); 1705 }); 1706 } catch (e) { console.error('balancing load failed', e); window.balancingData = window.balancingData || {}; } 1707 1708 // ZásobnÃky plynu EU (AGSI) â mapa (poslednà stav) + dennà vývoj 1709 try { 1710 const es = PG === 'plyn' ? await fetchAllRows('gas_storage_eu', 'observation_date, country_code, country_name, full_percent, gas_twh, trend', 'observation_date') : []; 1711 window.euStorageSeries = {}; window.euStorageNames = {}; 1712 es.forEach(r => { 1713 (window.euStorageSeries[r.country_code] = window.euStorageSeries[r.country_code] || []).push({ 1714 date: r.observation_date, 1715 full: r.full_percent == null ? null : parseFloat(r.full_percent), 1716 twh: r.gas_twh == null ? null : parseFloat(r.gas_twh) 1717 }); 1718 if (r.country_name) window.euStorageNames[r.country_code] = r.country_name; 1719 }); 1720 window.euStorageLatest = {}; window.euStorageLatestDate = null; 1721 Object.keys(window.euStorageSeries).forEach(code => { 1722 const s = window.euStorageSeries[code]; 1723 const last = s[s.length - 1]; 1724 if (last && last.date > (window.euStorageLatestDate || '')) window.euStorageLatestDate = last.date; 1725 if (code !== 'EU' && last) window.euStorageLatest[code] = { full: last.full, twh: last.twh, name: window.euStorageNames[code] || code }; 1726 }); 1727 window.euStorageHistory = {}; 1728 (window.euStorageSeries['EU'] || []).forEach(p => { 1729 if (p.full == null) return; 1730 const dt = new Date(p.date + 'T12:00:00'); const y = String(dt.getFullYear()); 1731 const doy = Math.round((dt - new Date(y + '-01-01T12:00:00')) / 86400000) + 1; 1732 (window.euStorageHistory[y] = window.euStorageHistory[y] || []).push({ week: Math.round(((doy - 1) / 7 + 1) * 100) / 100, value: p.full }); 1733 }); 1734 } catch (e) { console.error('eu storage load failed', e); window.euStorageSeries = {}; window.euStorageLatest = {}; } 1735 1736 // Skladba dovozu plynu do EU (ENTSOG + ALSI) â jen stránka Plyn 1737 try { 1738 const imp = (PG === 'plyn') ? await fetchAllRows('gas_imports_eu', 'trading_date, source, flow_gwh', 'trading_date') : []; 1739 window.euImports = {}; 1740 imp.forEach(r => { (window.euImports[r.source] = window.euImports[r.source] || []).push({ date: r.trading_date, gwh: r.flow_gwh == null ? null : parseFloat(r.flow_gwh) }); }); 1741 } catch (e) { console.error('imports load failed', e); window.euImports = {}; } 1742 1743 setMsg('NaÄÃtám ceny tepla, zprávy, ZEVO a spotÅebu energieâ¦'); 1744 const [{ data: heat }, storageAll, { data: news }, { data: zevo }, { data: fec }] = await Promise.all([ 1745 supa.from('heat_prices').select('*'), 1746 fetchAllRows('gas_storage', 'observation_date, full_percent', 'observation_date'), // celá AGSI historie 2020+ (sekce 9 + 10) 1747 supa.from('news_articles').select('*').order('published_at', { ascending: false }).limit(4), 1748 supa.from('zevo_annual').select('*').order('year', { ascending: false }), 1749 supa.from('energy_consumption_annual').select('*').order('year', { ascending: true }) 1750 ]); 1751 if (zevo) { 1752 window.zevoData = zevo; 1753 } 1754 window.fecData = fec || []; 1755 if (heat) { 1756 heat.forEach(r => { 1757 const y = String(r.year); 1758 if (!heatData.byYear[y]) heatData.byYear[y] = []; 1759 heatData.byYear[y].push({ kraj: r.region, price: parseFloat(r.price_czk_gj), gj: parseFloat(r.weighted_volume_gj), locality: r.location_count }); 1760 }); 1761 } 1762 if (storageAll && storageAll.length) { 1763 // Sekce 9 â vždy nejnovÄjšà hodnota z DB (dennà AGSI+ data, ~2dennà publikaÄnà zpoždÄnÃ) 1764 const lastRow = storageAll[storageAll.length - 1]; 1765 window.__lastStoragePct = parseFloat(lastRow.full_percent); 1766 const pctEl = document.getElementById('storage-pct'); 1767 if (pctEl) pctEl.textContent = window.__lastStoragePct.toFixed(1).replace('.', ',') + ' %'; 1768 const asofEl = document.getElementById('storage-asof'); 1769 if (asofEl) { 1770 const d = new Date(lastRow.observation_date + 'T12:00:00'); 1771 asofEl.textContent = 'ZaplnÄnost CZ (AGSI+ k ' + d.getDate() + '. ' + (d.getMonth() + 1) + '. ' + d.getFullYear() + ')'; 1772 } 1773 // MeziroÄnà srovnánÃ: stejné datum pÅed rokem (fallback nejbližšà den do ±5 dnÃ) 1774 const yoyEl = document.getElementById('storage-yoy'); 1775 if (yoyEl) { 1776 const byDate = {}; 1777 storageAll.forEach(r => { byDate[r.observation_date] = parseFloat(r.full_percent); }); 1778 const lastD = lastRow.observation_date; 1779 const targetISO = (String(+lastD.slice(0, 4) - 1)) + lastD.slice(4); 1780 let prevVal = null, prevDate = null; 1781 for (let off = 0; off <= 5 && prevVal === null; off++) { 1782 for (const s of (off === 0 ? [0] : [-off, off])) { 1783 const t = new Date(targetISO + 'T12:00:00'); t.setDate(t.getDate() + s); 1784 const iso = t.toISOString().slice(0, 10); 1785 if (byDate[iso] !== undefined) { prevVal = byDate[iso]; prevDate = t; break; } 1786 } 1787 } 1788 if (prevVal !== null) { 1789 const diff = window.__lastStoragePct - prevVal; 1790 const sign = diff >= 0 ? '+' : 'â'; 1791 // vÃc plynu = pozitivnà â zelená (ne cenová sémantika delta-up/down) 1792 const col = diff >= 0 ? (getCssVar('--gauge-high') || '#16a34a') : (getCssVar('--gauge-low') || '#dc2626'); 1793 yoyEl.innerHTML = 'PÅed rokem (' + prevDate.getDate() + '. ' + (prevDate.getMonth() + 1) + '. ' + prevDate.getFullYear() + '): ' 1794 + prevVal.toFixed(1).replace('.', ',') + ' % <span style="color:' + col + ';font-weight:600">' + sign + ' ' 1795 + Math.abs(diff).toFixed(1).replace('.', ',') + ' p. b.</span>'; 1796 } 1797 } 1798 // Sekce 10 â reálné dennà kÅivky po letech (x = kalendáÅnà týden, spojitÄ) 1799 const byYear = {}; 1800 storageAll.forEach(r => { 1801 const y = r.observation_date.slice(0, 4); 1802 const doy = Math.round((new Date(r.observation_date + 'T12:00:00') - new Date(y + '-01-01T12:00:00')) / 86400000) + 1; 1803 (byYear[y] = byYear[y] || []).push({ week: Math.round(((doy - 1) / 7 + 1) * 100) / 100, value: parseFloat(r.full_percent) }); 1804 }); 1805 if (Object.keys(byYear).length) storageHistory = byYear; 1806 } 1807 if (news) { 1808 const grid = document.getElementById('news-grid'); 1809 if (grid) { 1810 grid.innerHTML = news.map(n => { 1811 const d = new Date(n.published_at); 1812 const dateStr = d.getDate() + '. ' + (d.getMonth()+1) + '. ' + d.getFullYear(); 1813 const title = String(n.title || '').replace(/</g,'<'); 1814 const summary = String(n.summary || '').replace(/</g,'<'); 1815 return '<div class="news-card"><span class="news-tag ' + n.category + '">' + n.source + '</span><h3>' + title + '</h3><p class="news-summary">' + summary + '</p><div class="news-meta"><strong>' + dateStr + '</strong><span>·</span><a href="' + n.url + '" target="_blank">' + n.source + '</a></div></div>'; 1816 }).join(''); 1817 } 1818 } 1819 1820 // ===== AI komentáÅe (chart_commentary) â postrannà rámeÄek vedle obsahu sekce ===== 1821 try { 1822 const { data: comm } = await supa.from('chart_commentary').select('*'); 1823 if (comm && comm.length) { 1824 const st = document.createElement('style'); 1825 st.textContent = 1826 '.sec-flex{display:flex;gap:20px;align-items:stretch}' + 1827 '.sec-main{flex:1 1 auto;min-width:0}' + 1828 '.ai-side{flex:0 0 270px;box-sizing:border-box;padding:14px 16px;border:1px solid var(--border,#e4e4e7);' + 1829 'border-top:3px solid var(--brand,#00ca85);border-radius:10px;background:var(--bg-subtle,#f4f4f5);' + 1830 'font-size:13.5px;line-height:1.55;color:var(--text,#3f3f46)}' + 1831 '.ai-side .ai-tag{display:block;font-size:10.5px;font-weight:700;text-transform:uppercase;' + 1832 'letter-spacing:.05em;
1832color:var(--brand,#00ca85);margin-bottom:8px}' + 1833 '.ai-side .ai-when{display:block;margin-top:10px;font-size:11px;color:var(--text-muted,#71717a)}' + 1834 '.ai-side .ai-sep{border:none;border-top:1px solid var(--border,#e4e4e7);margin:16px 0 12px}' + 1835 '@media (max-width:980px){.sec-flex{flex-direction:column}.ai-side{flex:none;width:auto}}'; 1836 document.head.appendChild(st); 1837 // Seskup komentáÅe podle cÃlové sekce (sekce může nést vÃce klÃÄů, napÅ. data-comm="s1 s3a"). 1838 const bySection = new Map(); 1839 comm.forEach(c => { 1840 if (!c || !c.commentary) return; 1841 const key = String(c.section_id || '').toLowerCase(); 1842 const section = document.querySelector('section[data-comm~="' + key + '"]'); 1843 if (!section) return; 1844 if (!bySection.has(section)) bySection.set(section, []); 1845 bySection.get(section).push(c); 1846 }); 1847 bySection.forEach((list, section) => { 1848 const h2 = section.querySelector('h2'); 1849 if (!h2) return; 1850 list.sort((a, b) => String(a.section_id).localeCompare(String(b.section_id))); 1851 // pÅeskládánÃ: <h2> + <div.sec-flex>[ <div.sec-main>obsah</div> | <aside.ai-side> ] 1852 // data-ai-col: AI box jen vedle vymezeného sloupce (graf); zbytek sekce zůstane plnou Å¡ÃÅkou pod nÃm 1853 const scopedCol = section.querySelector('[data-ai-col]'); 1854 const flex = document.createElement('div'); 1855 flex.className = 'sec-flex'; 1856 const main = document.createElement('div'); 1857 main.className = 'sec-main'; 1858 if (scopedCol) { scopedCol.parentNode.insertBefore(flex, scopedCol); main.appendChild(scopedCol); } 1859 else { while (h2.nextSibling) main.appendChild(h2.nextSibling); } 1860 const aside = document.createElement('aside'); 1861 aside.className = 'ai-side'; 1862 aside.innerHTML = list.map((c, i) => { 1863 let when = ''; 1864 if (c.generated_at) { 1865 const dt = new Date(c.generated_at); 1866 when = 'vygenerováno ' + dt.getDate() + '. ' + (dt.getMonth() + 1) + '. ' + 1867 String(dt.getHours()).padStart(2, '0') + ':' + String(dt.getMinutes()).padStart(2, '0') + 1868 ' · automatický komentáÅ'; 1869 } 1870 return (i > 0 ? '<hr class="ai-sep">' : '') + 1871 '<span class="ai-tag">â¡ AI hodnocenÃ</span>' + 1872 String(c.commentary).replace(/</g, '<') + 1873 (when ? '<span class="ai-when">' + when + '</span>' : ''); 1874 }).join(''); 1875 flex.appendChild(main); 1876 flex.appendChild(aside); 1877 if (!scopedCol) section.appendChild(flex); 1878 }); 1879 } 1880 } catch (e) { console.error('commentary load failed', e); } 1881 1882 setMsg('Hotovo, vykresluji grafyâ¦'); 1883 return true; 1884} 1885 1886window.addEventListener("DOMContentLoaded", async function() { 1887 // NejdÅÃv pÅihlášenà â bez nÄj se data ani nezkoušà naÄÃtat. 1888 const stav = await authStav(); 1889 if (stav !== 'ok') { 1890 authUkazBranu(stav === 'bezpristupu' ? 'ÃÄet nemá k tomuto reportu pÅÃstup. Požádejte správce.' : ''); 1891 return; 1892 } 1893 try { await loadFromSupabase(); } catch (e) { console.error('Supabase load failed:', e); } 1894 const overlay = document.getElementById('supa-loading'); 1895 if (overlay) setTimeout(() => overlay.classList.add('hidden'), 300); 1896 __runOriginalInit(); 1897 authHlavicka(); 1898}); 1899 1900let euaChart = null; window.__euaRange = window.__euaRange || '12m'; window.__euaYZero = window.__euaYZero || false; window.__euaCcy = window.__euaCcy || 'EUR'; 1901function setEuaRange(btn, r) { 1902 window.__euaRange = r; 1903 if (btn && btn.parentElement) btn.parentElement.querySelectorAll('[data-euarange]').forEach(b => b.classList.toggle('active', b === btn)); 1904 drawEuaChart(); 1905} 1906function toggleEuaYZero(btn) { window.__euaYZero = !window.__euaYZero; if (btn) btn.classList.toggle('active', window.__euaYZero); drawEuaChart(); } 1907function toggleEuaCcy(btn) { window.__euaCcy = window.__euaCcy === 'EUR' ? 'CZK' : 'EUR'; if (btn) { btn.classList.toggle('active', window.__euaCcy === 'CZK'); btn.textContent = window.__euaCcy === 'CZK' ? '\u20ac' : 'K\u010d'; } drawEuaChart(); } 1908/* ââ Sekce 7: koneÄná spotÅeba energie ÄR podle nosiÄů (Eurostat FC_E) âââââââââââââââââ 1909 SkuteÄnost je roÄnà Åada z Eurostatu; výhledové scénáÅe se do stejné tabulky ukládajà 1910 pod vlastnÃm `scenario` a v grafu se odlišà průhlednostà + rámeÄkem. Pokud v DB žádný 1911 výhled nenÃ, sekce ukáže jen skuteÄnost a pÅepÃnaÄ scénáÅů se vůbec nezobrazÃ. */
1912let fecChart = null; 1913window.__fecView = 'pj'; 1914window.__fecScenario = null; 1915const FEC_CARRIERS = [ // poÅadà = zdola nahoru ve sloupci 1916 { key: 'odpad', label: 'Odpad (nerecyklovatelný)', color: '#c9a66b' }, 1917 { key: 'solarni', label: 'Solárnà termálnÃ', color: '#f59e0b' }, 1918 { key: 'ostatni', label: 'OstatnÃ', color: '#d4d4d8' }, 1919 { key: 'biopaliva', label: 'Biopaliva v dopravÄ', color: '#6b8e23' }, 1920 { key: 'vodik', label: 'VodÃk', color: '#1d4ed8' }, 1921 { key: 'teplo', label: 'Teplo', color: '#fdba74' }, 1922 { key: 'elektrina', label: 'ElektÅina', color: '#d946ef' }, 1923 { key: 'ropna_paliva', label: 'Ropná paliva', color: '#ef4444' }, 1924 { key: 'koks_plyny', label: 'Koks a vyrobené plyny', color: '#9ca3af' }, 1925 { key: 'zemni_plyn', label: 'Zemnà plyn', color: '#facc15' }, 1926 { key: 'hnede_uhli', label: 'HnÄdé uhlÃ', color: '#8b5a2b' }, 1927 { key: 'cerne_uhli', label: 'Äerné uhlÃ', color: '#18181b' }, 1928 { key: 'biomasa', label: 'Biomasa', color: '#16a34a' }, 1929 { key: 'bioplyn', label: 'Bioplyn', color: '#a6d96a' }, 1930 { key: 'energie_prostredi', label: 'Energie prostÅedÃ', color: '#e7ded3' } 1931]; 1932 1933// Äerné uhlà by na tmavém pozadà splynulo â v tmavém motivu ho posvÄtlÃme 1934function fecColor(c) { 1935 const dark = (document.documentElement.getAttribute('data-theme') || 'light') !== 'light'; 1936 if (c.key === 'cerne_uhli' && dark) return '#52525b'; 1937 return paletteColor(c.color); 1938} 1939 1940function fecScenarios() { 1941 const s = [...new Set((window.fecData || []).map(r => r.scenario))].filter(x => x !== 'skutecnost'); 1942 return s.sort(); 1943} 1944function setFecView(v) { 1945 window.__fecView = v; 1946 document.querySelectorAll('[data-fecview]').forEach(b => b.classList.toggle('active', b.getAttribute('data-fecview') === v)); 1947 drawFecChart(); 1948} 1949function setFecScenario(s) { 1950 window.__fecScenario = s; 1951 document.querySelectorAll('[data-fecscen]').forEach(b => b.classList.toggle('active', b.getAttribute('data-fecscen') === String(s))); 1952 drawFecChart(); 1953} 1954function fecScenarioLabel(s) { 1955 if (s === 'wem') return 'WEM â se stávajÃcÃmi opatÅenÃmi'; 1956 if (s === 'wam') return 'WAM â s dodateÄnými opatÅenÃmi'; 1957 return s; 1958} 1959window.__fecFill = false; 1960function setFecFill(on) { 1961 window.__fecFill = !!on; 1962 document.querySelectorAll('[data-fecfill]').forEach(b => b.classList.toggle('active', (b.getAttribute('data-fecfill') === '1') === !!on)); 1963 drawFecChart(); 1964} 1965 1966function drawFecChart() { 1967 const el = document.getElementById('fec-chart'); if (!el) return; 1968 const rows = window.fecData || []; 1969 if (!rows.length) return; 1970 const muted = getCssVar('--text-muted') || '#71717a'; 1971 const border = getCssVar('--border') || '#e4e4e7'; 1972 const strong = getCssVar('--text-strong') || '#09090b'; 1973 const bgCard = getCssVar('--bg-card') || '#fff'; 1974 1975 // pÅepÃnaÄ scénáÅů se vykreslà jen když v DB nÄjaký výhled je 1976 const scens = fecScenarios(); 1977 const btnBox = document.getElementById('fec-scen-buttons'); 1978 const scenLabel = document.getElementById('fec-scen-label'); 1979 if (btnBox && !btnBox.dataset.built) { 1980 if (scens.length) { 1981 if (scenLabel) scenLabel.hidden = false; 1982 btnBox.innerHTML = scens.map(s => 1983 '<button data-fecscen="' + s + '" onclick="setFecScenario(\'' + s + '\')">' + fecScenarioLabel(s) + '</button>').join('') 1984 + '<button data-fecscen="null" class="active" onclick="setFecScenario(null)">Bez výhledu</button>' 1985 + '<span class="label-small" style="margin-left:12px">Mezilehlé roky:</span>' 1986 + '<button data-fecfill="0" class="active" onclick="setFecFill(false)">Nechat prázdné</button>' 1987 + '<button data-fecfill="1" onclick="setFecFill(true)">DopoÄÃtat lineárnÄ</button>'; 1988 } 1989 btnBox.dataset.built = '1'; 1990 } 1991 1992 const hist = rows.filter(r => r.scenario === 'skutecnost'); 1993 const sc = window.__fecScenario; 1994 const proj = sc ? rows.filter(r => r.scenario === sc) : []; 1995 const histYears = [...new Set(hist.map(r => r.year))].sort((a, b) => a - b); 1996 const lastHist = histYears[histYears.length - 1]; 1997 const modelYears = [...new Set(proj.map(r => r.year))].filter(y => y > lastHist).sort((a, b) => a - b); 1998 // osa je ROÄNà i do budoucna; model ale poÄÃtá jen v pÄtiletých krocÃch (TIMES-CZ), 1999 // takže mezilehlé roky jsou buÄ prázdné, nebo â po zapnutà â lineárnÄ dopoÄtené. 2000 const lastYear = modelYears.length ? modelYears[modelYears.length - 1] : lastHist; 2001 const years = []; 2002 for (let y = histYears[0]; y <= lastYear; y++) years.push(y); 2003 const isProj = y => y > lastHist; 2004 const isModel = y => modelYears.indexOf(y) > -1; 2005 const isFill = y => isProj(y) && !isModel(y); 2006 2007 const val = {}; 2008 rows.forEach(r => { val[r.scenario + '|' + r.year + '|' + r.carrier] = parseFloat(r.pj); }); 2009 const raw = (y, c) => val[(isProj(y) ? sc : 'skutecnost') + '|' + y + '|' + c]; 2010 const get = (y, c) => { 2011 if (!isFill(y)) return raw(y, c) || 0; 2012 if (!window.__fecFill) return 0; 2013 const lo = [lastHist].concat(modelYears).filter(x => x < y).pop(); 2014 const hi = modelYears.find(x => x > y); 2015 if (lo === undefined || hi === undefined) return 0; 2016 const a = raw(lo, c) || 0, b = raw(hi, c) || 0; 2017 return a + (b - a) * (y - lo) / (hi - lo); 2018 }; 2019 const totals = {};
2019 years.forEach(y => { totals[y] = FEC_CARRIERS.reduce((a, c) => a + get(y, c.key), 0); }); 2020 2021 const pct = window.__fecView === 'pct'; 2022 const alpha = (hex, a) => { 2023 const h = hex.replace('#', ''); 2024 const n = parseInt(h.length === 3 ? h.split('').map(x => x + x).join('') : h, 16); 2025 return 'rgba(' + ((n >> 16) & 255) + ',' + ((n >> 8) & 255) + ',' + (n & 255) + ',' + a + ')'; 2026 }; 2027 const datasets = FEC_CARRIERS 2028 .filter(c => years.some(y => get(y, c.key) > 0)) // nosiÄ bez dat se nezobrazuje 2029 .map(c => ({ 2030 label: c.label, 2031 data: years.map(y => pct ? (totals[y] ? get(y, c.key) / totals[y] * 100 : 0) : get(y, c.key)), 2032 backgroundColor: years.map(y => isFill(y) ? alpha(fecColor(c), 0.28) : isProj(y) ? alpha(fecColor(c), 0.6) : fecColor(c)), 2033 borderColor: years.map(y => isProj(y) ? muted : 'transparent'), 2034 borderWidth: years.map(y => isModel(y) ? 0.5 : 0), 2035 stack: 'fec', maxBarThickness: 40, $raw: c.key 2036 })); 2037 2038 if (fecChart) fecChart.destroy(); 2039 fecChart = new Chart(el.getContext('2d'), { 2040 type: 'bar', 2041 data: { labels: years.map(String), datasets: datasets }, 2042 options: { 2043 responsive: true, maintainAspectRatio: false, animation: false, 2044 interaction: { mode: 'index', intersect: false }, 2045 plugins: { 2046 legend: { display: true, position: 'top', reverse: true, 2047 labels: { color: muted, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12, boxHeight: 8, padding: 8, font: { size: 10, weight: '500' } } }, 2048 tooltip: { 2049 backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8, 2050 itemSort: (a, b) => b.datasetIndex - a.datasetIndex, 2051 filter: item => item.parsed.y > 0.05, 2052 callbacks: { 2053 title: items => { 2054 const y = years[items[0].dataIndex]; 2055 if (isFill(y)) return y + ' â LINEÃRNà DOPOÄET (model tento rok nepoÄÃtá)'; 2056 return y + (isProj(y) ? ' â výhled ' + fecScenarioLabel(sc) : ''); 2057 }, 2058 label: c => { 2059 const y = years[c.dataIndex]; 2060 const raw = get(y, datasets[c.datasetIndex].$raw); 2061 return c.dataset.label + ': ' + raw.toFixed(1).replace('.', ',') + ' PJ' 2062 + (totals[y] ? ' (' + (raw / totals[y] * 100).toFixed(1).replace('.', ',') + ' %)' : ''); 2063 }, 2064 footer: items => { 2065 const y = years[items[0].dataIndex]; 2066 return 'Celkem: ' + totals[y].toFixed(1).replace('.', ',') + ' PJ'; 2067 } 2068 } 2069 } 2070 }, 2071 scales: { 2072 x: { stacked: true, grid: { display: false }, ticks: { color: muted, font: { size: 10 }, autoSkip: false, maxRotation: 60 } }, 2073 y: { stacked: true, beginAtZero: true, max: pct ? 100 : undefined, 2074 title: { display: true, text: pct ? 'podÃl na koneÄné spotÅebÄ (%)' : 'koneÄná spotÅeba energie (PJ/rok)', color: muted }, 2075 grid: { color: border }, 2076 ticks: { color: muted, callback: v => pct ? v + ' %' : v.toLocaleString('cs-CZ') } } 2077 } 2078 } 2079 }); 2080 renderFecTable(years.filter(y => !isFill(y) || window.__fecFill), get, totals, isProj, isFill); 2081} 2082 2083function renderFecTable(years, get, totals, isProj, isFill) { 2084 const th = document.getElementById('fec-thead'), tb = document.getElementById('fec-tbody'); 2085 if (!th || !tb) return; 2086 const used = FEC_CARRIERS.filter(c => years.some(y => get(y, c.key) > 0)); 2087 th.innerHTML = '<th>Rok</th>' + used.map(c => '<th class="num">' + c.label + '</th>').join('') + '<th class="num">Celkem</th>'; 2088 const f = v => v === 0 ? 'â' : v.toFixed(1).replace('.', ','); 2089 tb.innerHTML = years.slice().reverse().map(y => 2090 '<tr><td><strong>' + y + '</strong>' + (isFill(y) ? ' <span style="color:var(--text-muted);font-size:11px">dopoÄet</span>' 2091 : isProj(y) ? ' <span style="color:var(--text-muted);font-size:11px">výhled</span>' : '') + '</td>' 2092 + used.map(c => '<td class="num"' + (isFill(y) ? ' style="opacity:.6"' : '') + '>' + f(get(y, c.key)) + '</td>').join('') 2093 + '<td class="num"><strong>' + f(totals[y]) + '</strong></td></tr>').join(''); 2094} 2095 2096function drawEuaChart() { 2097 const cv = document.getElementById('eua-chart'); 2098 if (!cv || typeof Chart === 'undefined') return; 2099 const pts = (typeof getSeriesPoints === 'function') ? getSeriesPoints('eua') : []; 2100 if (!pts.length) return; 2101 const r = window.__euaRange || '12m'; 2102 let start = -Infinity; 2103 if (r !== 'all') { const last = new Date(pts[pts.length-1].date); const d = new Date(last); d.setMonth(d.getMonth() - ({'3m':3,'12m':12}[r] || 12)); start = d.getTime(); } 2104 const czk = window.__euaCcy === 'CZK'; 2105 const unit = czk ? 'CZK/t' : 'EUR/t'; 2106 const conv = (v, date) => czk ? v * (fxOnDate(date) || fxLatest() || 25) : v; 2107 const data = pts.filter(p => new Date(p.date).getTime() >= start).map(p => ({ x: new Date(p.date).getTime(), y: conv(p.value, p.date) })); 2108 const muted = getCssVar('--text-muted') || '#71717a', border = getCssVar('--border') || '#e4e4e7', 2109 strong = getCssVar('--text-strong') || '#09090b', bgCard = getCssVar('--bg-card') || '#fff'; 2110 const col = (typeof getSeriesColor === 'function') ? getSeriesColor
2110('eua') : '#3B5BC6'; 2111 if (euaChart) euaChart.destroy(); 2112 euaChart = new Chart(cv.getContext('2d'), { 2113 type: 'line', data: { datasets: [{ label: 'EUA', data, borderColor: col, backgroundColor: col + '14', borderWidth: 2, pointRadius: 0, pointHoverRadius: 6, pointHoverBorderWidth: 2, tension: 0.05, fill: false, spanGaps: true, parsing: false }] }, 2114 options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'nearest', intersect: false }, 2115 plugins: { legend: { display: false }, tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, borderColor: border, padding: 12, cornerRadius: 8, displayColors: true, boxPadding: 4, 2116 callbacks: { title: items => items.length ? new Date(items[0].parsed.x).toLocaleDateString('cs-CZ') : '', label: ctx => ctx.parsed.y == null ? '\u2014' : ctx.parsed.y.toFixed(2).replace('.', ',') + ' ' + unit } } }, 2117 scales: { y: { type: 'linear', position: 'left', beginAtZero: window.__euaYZero, title: { display: true, text: unit, color: muted, font: {size:11} }, grid: { color: border, lineWidth: 1 }, ticks: { color: muted, font: {size:11} } }, 2118 x: { type: 'linear', bounds: 'data', grid: { display: false }, title: { display: false }, afterDataLimits: captureXDataLimits, afterBuildTicks: calendarXTicks, ticks: { color: muted, autoSkip: false, maxRotation: 0, font: {size:11}, callback: adaptiveXTick } } } } 2119 }); 2120} 2121 2122let oilChart = null; window.__oilRange = window.__oilRange || '12m'; window.__oilYZero = window.__oilYZero || false; 2123window.__oilUnit = window.__oilUnit || (function(){ try { return localStorage.getItem('oilUnit') || 'usd_bbl'; } catch(e){ return 'usd_bbl'; } })(); 2124// Jednotky ropy â ovÄÅené konstanty (EIA/BP): 1 bbl = 158,987 l; bbl/t Brent 7,53 / WTI 7,61; 6,12 GJ/bbl 2125const OIL_BBL_PER_T = { brent: 7.53, wti: 7.61 }; 2126const OIL_L_PER_BBL = 158.987, OIL_GJ_PER_BBL = 6.12; 2127const OIL_UNITS = { 2128 usd_bbl: { label: 'USD/bbl', dec: 2, conv: function(v){ return v; } }, 2129 usd_t: { label: 'USD/t', dec: 0, conv: function(v, sid){ return v * (OIL_BBL_PER_T[sid] || 7.33); } }, 2130 usd_gj: { label: 'USD/GJ', dec: 2, conv: function(v){ return v / OIL_GJ_PER_BBL; } }, 2131 czk_bbl: { label: 'K\u010d/bbl', dec: 0, czk: true, conv: function(v, sid, date){ return v * (usdCzkOn(date) || usdCzkLatest() || 21); } }, 2132 czk_l: { label: 'K\u010d/l', dec: 2, czk: true, conv: function(v, sid, date){ return v * (usdCzkOn(date) || usdCzkLatest() || 21) / OIL_
2132L_PER_BBL; } } 2133}; 2134function oilUnit(){ return OIL_UNITS[window.__oilUnit] ? window.__oilUnit : 'usd_bbl'; } 2135function oilUnitLabel(){ return OIL_UNITS[oilUnit()].label; } 2136function oilConv(v, sid, date){ if (v == null || !isFinite(v)) return v; return OIL_UNITS[oilUnit()].conv(v, sid, date); } 2137function oilFmt(v){ return (v == null || !isFinite(v)) ? '\u2014' : fmtNum(v, OIL_UNITS[oilUnit()].dec); } 2138function setOilUnit(u){ if (!OIL_UNITS[u]) return; window.__oilUnit = u; try { localStorage.setItem('oilUnit', u); } catch(e){} const sel = document.getElementById('oil-unit'); if (sel && sel.value !== u) sel.value = u; if (typeof updateLatestPriceTables === 'function') updateLatestPriceTables(); drawOilChart(); } 2139function setOilRange(btn, r) { 2140 window.__oilRange = r; 2141 if (btn && btn.parentElement) btn.parentElement.querySelectorAll('[data-oilrange]').forEach(b => b.classList.toggle('active', b === btn)); 2142 drawOilChart(); 2143} 2144function toggleOilYZero(btn) { window.__oilYZero = !window.__oilYZero; if (btn) btn.classList.toggle('active', window.__oilYZero); drawOilChart(); } 2145function drawOilChart() { 2146 const cv = document.getElementById('oil-chart'); 2147 if (!cv || typeof Chart === 'undefined') return; 2148 const SERIES = [{ id: 'brent', label: 'Brent' }, { id: 'wti', label: 'WTI' }]; 2149 const all = SERIES.map(sr => ({ ...sr, pts: (typeof getSeriesPoints === 'function') ? getSeriesPoints(sr.id) : [] })).filter(sr => sr.pts.length); 2150 if (!all.length) return; 2151 const r = window.__oilRange || '12m'; 2152 let start = -Infinity; 2153 const anchor = all[0].pts; 2154 const lastD = new Date(anchor[anchor.length-1].date); 2155 if (r === 'ytd') { start = Date.UTC(lastD.getUTCFullYear(), 0, 1); } 2156 else if (r !== 'all') { const d = new Date(lastD); d.setMonth(d.getMonth() - ({'1m':1,'3m':3,'6m':6,'12m':12}[r] || 12)); start = d.getTime(); } 2157 const muted = getCssVar('--text-muted') || '#71717a', border = getCssVar('--border') || '#e4e4e7', 2158 strong = getCssVar('--text-strong') || '#09090b', bgCard = getCssVar('--bg-card') || '#fff'; 2159 const datasets = all.map(sr => { 2160 const col = (typeof getSeriesColor === 'function') ? getSeriesColor(sr.id) : '#0d9488'; 2161 return { label: sr.label, data: sr.pts.filter(pp => new Date(pp.date).getTime() >= start).map(pp => ({ x: new Date(pp.date).getTime(), y: oilConv(pp.value, sr.id, pp.date) })), borderColor: col, backgroundColor: col + '14', borderWidth: 2, pointRadius: 0, pointHoverRadius: 6, pointHoverBorderWidth: 2, tension: 0.05, fill: false, spanGaps: true, parsing: false }; 2162 }); 2163 if (oilChart) oilChart.destroy(); 2164 oilChart = new Chart(cv.getContext('2d'), { 2165 type: 'line', data: { datasets }, 2166 options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'nearest', intersect: false }, 2167 plugins: { legend: { display: true, position: 'top', labels: { color: muted, usePointStyle: true, pointStyle: 'line', boxWidth: 22, padding: 14, font: {size:12} } }, 2168 tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, borderColor: border, padding: 12, cornerRadius: 8, displayColors: true, boxPadding: 4, 2169 callbacks: { title: items => items.length ? new Date(items[0].parsed.x).toLocaleDateString('cs-CZ') : '', label: ctx => ctx.dataset.label + ': ' + (ctx.parsed.y == null ? '\u2014' : fmtNum(ctx.parsed.y, OIL_UNITS[oilUnit()].dec) + ' ' + oilUnitLabel()) } } }, 2170 scales: { y: { type: 'linear', position: 'left', beginAtZero: window.__oilYZero, title: { display: true, text: oilUnitLabel(), color: muted, font: {size:11} }, grid: { color: border, lineWidth: 1 }, ticks: { color: muted, font: {size:11} } }, 2171 x: { type: 'linear', bounds: 'data', grid: { display: false }, title: { display: false }, afterDataLimits: captureXDataLimits, afterBuildTicks: calendarXTicks, ticks: { color: muted, autoSkip: false, maxRotation: 0, font: {size:11}, callback: adaptiveXTick } } } } 2172 }); 2173} 2174function __runOriginalInit() { 2175 // Dynamicky nastav dneÅ¡nà datum v hlaviÄce + patiÄce (Äeská lokalizace bez závislosti na Intl) 2176 (function updateDateLabels() { 2177 const days = ['nedÄle','pondÄlÃ','úterý','stÅeda','Ätvrtek','pátek','sobota']; 2178 const months = ['ledna','února','bÅezna','dubna','kvÄtna','Äervna','Äervence','srpna','záÅÃ','ÅÃjna','listopadu','prosince']; 2179 const d = new Date(); 2180 const dayName = days[d.getDay()]; 2181 const dayNum = d.getDate(); 2182 const monthName = months[d.getMonth()]; 2183 const year = d.getFullYear(); 2184 const headerEl = document.getElementById('report-date'); 2185 if (headerEl) headerEl.textContent = `${dayName} ${dayNum}. ${monthName} ${year}`; 2186 const footerEl = document.getElementById('generated-at'); 2187 if (footerEl) footerEl.textContent = `Vygenerováno: ${dayNum}. ${monthName} ${year}.`; 2188 })(); 2189 updateThemeIcon();
2190 // Jediné tlaÄÃtko âAktualizovat data": spustà nový výpoÄet dat (je-li dostupný), 2191 // poÄká na dokonÄenà a zobrazà Äerstvá data; jinak aspoÅ pÅenaÄte aktuálnà stav z DB. 2192 (function setupRefreshButtons() { 2193 const btn = document.getElementById('refresh-btn'); 2194 if (btn) btn.title = 'Spustà nový výpoÄet dat a po dokonÄenà (~1â2 min) zobrazà Äerstvá data.'; 2195 })(); 2196 // Aplikuj barevné teÄky na toggle tlaÄÃtka 2197 document.querySelectorAll('.range-controls button[data-dot]').forEach(b => { 2198 b.style.setProperty('--dot-color', b.getAttribute('data-dot')); 2199 }); 2200 // OTE day picker â default na dneÅ¡ek (pokud máme data), jinak poslednà dostupný 2201 const otePicker = document.getElementById('ote-date-picker'); 2202 if (otePicker) { 2203 const days = oteAvailableDays(); 2204 const todayISO = new Date().toISOString().slice(0,10); 2205 if (days.includes(todayISO)) otePicker.value = todayISO; 2206 else if (days.length) otePicker.value = days[days.length-1]; 2207 if (days.length) { otePicker.min = days[0]; otePicker.max = days[days.length-1]; } 2208 } 2209 const oteCountEl = document.getElementById('ote-history-count'); 2210 if (oteCountEl) oteCountEl.textContent = Object.keys(window.ote15Data || {}).length; 2211 // Každé volánà izolovanÄ â chybÄjÃcà sekce (jiná stránka) nesmà shodit zbytek initu. 2212 const __s = (fn) => { try { fn(); } catch (e) { console.warn('init skip:', e && e.message); } }; 2213 __s(drawOteChart); 2214 __s(() => { if (typeof drawFecChart === 'function') drawFecChart(); }); 2215 __s(() => drawStorageGauge(window.__lastStoragePct || 33)); 2216 __s(updateLatestPriceTables); 2217 __s(updateFreshnessBadges); 2218 __s(updateOverallFreshness); 2219 __s(() => drawPriceChart('el', '1m')); 2220 __s(() => drawPriceChart('gas', '1m')); 2221 __s(drawEuaChart); 2222 { const _ou = document.getElementById('oil-unit'); if (_ou) _ou.value = oilUnit(); } 2223 __s(drawOilChart); 2224 __s(drawStorageHistoryChart); 2225 __s(drawHeatChart); 2226 __s(drawTapChart); 2227 __s(() => { if (typeof drawZevoChart === 'function') drawZevoChart(); }); 2228 __s(drawSpotHourlyChart); 2229 __s(drawSpotHourChart); 2230 __s(drawSpotDowChart); 2231 __s(drawForwardSpotChart); 2232 __s(drawBalancingChart); 2233 __s(drawEuStorage); 2234 __s(drawImportsChart); 2235 __s(() => { 2236 if (typeof setEntsoeDate === 'function') { setEntsoeDate('latest'); } 2237 else { drawEntsoeGenChart(); drawEntsoeFlowsChart(); } 2238 }); 2239 __s(() => { if (typeof setEntsoeUnitDate === 'function') setEntsoeUnitDate('latest'); }); 2240 __s(() => { if (typeof drawHeatFuelChart === 'function') drawHeatFuelChart(); }); 2241 __s(() => { if (typeof drawHeatPlantsChart === 'function') drawHeatPlantsChart(); }); 2242 // VypiÅ¡ poÄet bodů v databázi 2243 const dataCountEl = document.getElementById('data-counter'); 2244 if (dataCountEl) { 2245 const parts = SERIES_DEF.map(s => { 2246 const n = getSeriesPoints(s.id).length; 2247 return s.label + ' = <strong>' + n + '</strong>'; 2248 }); 2249 dataCountEl.innerHTML = parts.join(' · '); 2250 } 2251 // init custom date pickers pro oba grafy (el-sekce z el27, gas-sekce z gas27) 2252 function initDatePickers(suffix, sid) { 2253 const pts = getSeriesPoints(sid); 2254 if (!pts.length) return; 2255 const first = pts[0].date, last = pts[pts.length-1].date; 2256 const fromEl = document.getElementById('date-from-' + suffix); 2257 const toEl = document.getElementById('date-to-' + suffix); 2258 if (fromEl) { fromEl.min = first; fromEl.max = last; } 2259 if (toEl) { toEl.min = first; toEl.max = last; toEl.value = last; } 2260 } 2261 initDatePickers('el', 'el27'); 2262 initDatePickers('gas', 'gas27'); 2263} 2264 2265let oteChart = null; 2266let oteLoadingDate = null; 2267function addDays(dateStr, n) { 2268 const d = new Date(dateStr); d.setDate(d.getDate() + n); 2269 return d.toISOString().slice(0,10); 2270} 2271async function drawOteChart() { 2272 const ctx = document.getElementById('ote-chart').getContext('2d'); 2273 const accent = getCssVar('--accent') || '#dc2626'; 2274 const muted = getCssVar('--text-muted') || '#71717a'; 2275 const border = getCssVar('--border') || '#e4e4e7'; 2276 const strong = getCssVar('--text-strong') || '#09090b'; 2277 const bgCard = getCssVar('--bg-card') || '#fff'; 2278 const picker = document.getElementById('ote-date-picker'); 2279 const selectedDate = (picker && picker.value) ? picker.value : oteData.date; 2280 const tomorrowDate = addDays(selectedDate, 1); 2281 const todayISO = new Date().toISOString().slice(0,10); 2282 const noteEl = document.getElementById('ote-date-note'); 2283 // Pokus se mÃt data pro vybraný den 2284 const hasData = Array.isArray(window.ote15Data[selectedDate]) && window.ote15Data[selectedDate].some(v => v !== null && v !== undefined && isFinite(v)); 2285 if (!hasData) { 2286 if (window.cowork && window.cowork.callMcpTool) { 2287 // V Cowork prostÅedà lze den dotáhnout z OTE-CR 2288 if (noteEl) noteEl.textContent = 'NaÄÃtám ' + selectedDate + ' z OTE-CRâ¦'; 2289 if (oteLoadingDate !== selectedDate) { 2290 oteLoadingDate = selectedDate; 2291 const r = await loadOteFromCR(selectedDate); 2292 oteLoadingDate = null; 2293 if (!r.ok && noteEl) noteEl.textContent = 'â ' + r.error; 2294 else if (noteEl) noteEl.textContent = 'â Stáhnuto z OTE-CR a uloženo do databáze.'; 2295 } 2296 } else {
2297 // Mimo Cowork: pro tento den data nejsou â skoÄ na poslednà dostupný den 2298 const days = oteAvailableDays(); 2299 if (days.length && selectedDate !== days[days.length-1]) { 2300 if (picker) picker.value = days[days.length-1]; 2301 return drawOteChart(); 2302 } 2303 if (noteEl) noteEl.textContent = 'Pro tento den nejsou data; zobrazuji poslednà dostupný den.'; 2304 } 2305 } else { 2306 if (noteEl) noteEl.textContent = ''; 2307 } 2308 // Auto-fetch zÃtÅek pokud uživatel volà dneÅ¡ek a zÃtÅek tam nenà 2309 if (selectedDate === todayISO && !window.ote15Data[tomorrowDate]) { 2310 if (oteLoadingDate !== tomorrowDate) { 2311 oteLoadingDate = tomorrowDate; 2312 loadOteFromCR(tomorrowDate).then(r => { 2313 oteLoadingDate = null; 2314 if (r.ok) drawOteChart(); 2315 }); 2316 } 2317 } 2318 // Sestav datasety 2319 const datasets = []; 2320 const dayData = window.ote15Data[selectedDate]; 2321 // ===== Statistiky (Base/Peak/Offpeak) + datum â poÄÃtané z vybraného dne, ignorujà chybÄjÃcà hodnoty ===== 2322 const avgValid = arr => { const v = (arr || []).filter(x => x !== null && x !== undefined && isFinite(x)); return v.length ? v.reduce((a, b) => a + b, 0) / v.length : null; }; 2323 const fmt2 = v => v === null ? 'â' : v.toFixed(2).replace('.', ','); 2324 const baseEl = document.getElementById('ote-base'), peakEl = document.getElementById('ote-peak'), offEl = document.getElementById('ote-offpeak'), labelEl = document.getElementById('ote-base-label'); 2325 if (dayData) { 2326 const peakSlots = dayData.filter((_, i) => { const h = Math.floor(i / 4); return h >= 8 && h < 20; }); 2327 const offSlots = dayData.filter((_, i) => { const h = Math.floor(i / 4); return !(h >= 8 && h < 20); }); 2328 const base = avgValid(dayData), peak = avgValid(peakSlots), off = avgValid(offSlots); 2329 const missing = dayData.filter(x => x === null || x === undefined || !isFinite(x)).length; 2330 const _r = _czk('ote') ? (fxOnDate(selectedDate) || 1) : 1; 2331 if (baseEl) baseEl.textContent = fmt2(base == null ? null : base * _r) + ' ' + ccyUnit('ote'); 2332 if (peakEl) peakEl.textContent = fmt2(peak == null ? null : peak * _r); 2333 if (offEl) offEl.textContent = fmt2(off == null ? null : off * _r); 2334 if (labelEl) { 2335 const dstr = new Date(selectedDate + 'T00:00:00').toLocaleDateString('cs-CZ'); 2336 labelEl.textContent = 'Base Load â ' + dstr + (selectedDate === todayISO ? ' (dnes)' : '') + (missing ? ' · chybà ' + missing + '/96 hodnot' : ''); 2337 } 2338 } else { 2339 if (baseEl) baseEl.textContent = 'â ' + ccyUnit('ote'); 2340 if (peakEl) peakEl.textContent = 'â'; 2341 if (offEl) offEl.textContent = 'â'; 2342 if (labelEl) labelEl.textContent = 'Base Load â ' + new Date(selectedDate + 'T00:00:00').toLocaleDateString('cs-CZ') + (selectedDate === todayISO ? ' (dnes)' : '') + ' · data nedostupná'; 2343 } 2344 if (dayData) { 2345 datasets.push({ 2346 label: selectedDate + ' (15min)', 2347 data: dayData.map(v => cvDate(v, selectedDate, 'ote')), borderColor: accent, backgroundColor: hexToRgba(accent, 0.12), 2348 fill: true, tension: 0.25, pointRadius: 0, pointHoverRadius: 5, borderWidth: 2, spanGaps: true 2349 }); 2350 } 2351 const nextData = window.ote15Data[tomorrowDate]; 2352 if (nextData) { 2353 const lighter = '#f97316'; // zÃtÅek = oranžová pro odliÅ¡enà 2354 datasets.push({ 2355 label: tomorrowDate + ' (D+1)', 2356 data: nextData.map(v => cvDate(v, tomorrowDate, 'ote')), borderColor: lighter, backgroundColor: hexToRgba(lighter, 0.08), 2357 fill: false, tension: 0.25, pointRadius: 0, pointHoverRadius: 5, borderWidth: 2, borderDash: [4, 4] 2358 }); 2359 } 2360 if (oteChart) oteChart.destroy(); 2361 oteChart = new Chart(ctx, { 2362 type: 'line', 2363 data: { labels: oteData.hours, datasets: datasets }, 2364 options: { 2365 responsive: true, maintainAspectRatio: false, 2366 interaction: { mode: 'index', intersect: false }, 2367 plugins: { 2368 legend: { display: true, position: 'top', labels: { color: muted, font: {size: 12, weight: '500'}, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12 } }, 2369 tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8, 2370 callbacks: { label: c => c.dataset.label + ': ' + (c.parsed.y == null ? 'â' : c.parsed.y.toFixed(2).replace('.', ',') + ' ' + ccyUnit('ote')) } } 2371 }, 2372 scales: { 2373 y: { title: { display: true, text: ccyUnit('ote'), color: muted }, grid: { color: border, lineWidth: 1 }, ticks: { color: muted } }, 2374 x: { title: { display: true, text: 'Äas (15min intervaly)', color: muted }, grid: { display: false }, ticks: { color: muted, autoSkip: true, maxTicksLimit: 13, callback: function(v, i) { return i % 4 === 0 ? oteData.hours[i] : ''; } } } 2375 } 2376 } 2377 }); 2378} 2379function hexToRgba(hex, alpha) { 2380 hex = (hex || '#000').replace('#',''); 2381 if (hex.length === 3) hex = hex.split('').map(c=>c+c).join(''); 2382 const num = parseInt(hex, 16); 2383 return `rgba(${(num>>16)&255},${(num>>8)&255},${num&255},${alpha})`; 2384} 2385 2386function drawStorageGauge(pct) { 2387 window.__lastStoragePct = pct; 2388 const canvas = document.getElementById('storage-gauge'); 2389 const ctx = canvas.getContext('2d'); 2390 // HiDPI/retina: render v plném rozliÅ¡enà displeje, jinak je kresba rozmazaná 2391 const dpr = window.devicePixelRatio || 1; 2392 const cssW = 220, cssH = 220; 2393 canvas.width = cssW * dpr; 2394 canvas.height = cssH * dpr; 2395 canvas.style.width = cssW + 'px'; 2396 canvas.style.height = cssH + 'px';
2397 ctx.setTransform(dpr, 0, 0, dpr, 0, 0); 2398 ctx.clearRect(0, 0, cssW, cssH); 2399 const cx = cssW / 2, cy = cssH / 2; 2400 const r = 88; 2401 ctx.lineWidth = 18; 2402 ctx.lineCap = 'round'; 2403 // Background ring 2404 ctx.beginPath(); 2405 ctx.strokeStyle = getCssVar('--bg-subtle') || '#e4e4e7'; 2406 ctx.arc(cx, cy, r, -Math.PI/2, 1.5*Math.PI); 2407 ctx.stroke(); 2408 // Filled portion 2409 const angle = -Math.PI/2 + (pct/100) * 2*Math.PI; 2410 ctx.beginPath(); 2411 ctx.strokeStyle = pct < 30 ? (getCssVar('--gauge-low') || '#dc2626') : (pct < 60 ? (getCssVar('--gauge-mid') || '#f59e0b') : (getCssVar('--gauge-high') || '#16a34a')); 2412 ctx.arc(cx, cy, r, -Math.PI/2, angle); 2413 ctx.stroke(); 2414 // Hodnota se zobrazuje vedle v #storage-pct â v canvasu ji nekreslÃme (duplikovala se) 2415} 2416 2417function filterSeriesByRange(seriesId, range) { 2418 const start = rangeStart(range); 2419 const end = TODAY; 2420 return getSeriesPoints(seriesId).filter(p => { 2421 const dt = new Date(p.date); 2422 return dt >= start && dt <= end; 2423 }); 2424} 2425 2426function filterSeriesByDates(seriesId, fromStr, toStr) { 2427 return getSeriesPoints(seriesId).filter(p => p.date >= fromStr && p.date <= toStr); 2428} 2429 2430// Barvy: elektÅina = Äervené odstÃny, plyn = žluto-zlaté odstÃny. 2431// Dva paletové fallbacky pro light/dark â JS vybere podle aktivnÃho tématu. 2432const COLOR_PALETTE = { 2433 light: { 2434 el26: '#7f1d1d', // tmavÄ bordó 2435 el27: '#dc2626', // primárnà Äervená 2436 el28: '#ef4444', // stÅednà Äervená 2437 el29: '#fb7185', // růžovo-Äervená 2438 el_nextyear: '#ff1a1a', // výrazná Äervená â next year elektÅina 2439 gasTTF: '#b45309', // tmavÄ zlatá 2440 gas27: '#f59e0b', // amber 2441 gas28: '#fbbf24', // žlutá 2442 gas29: '#fde047', // svÄtle žlutá 2443 gas_nextyear: '#ffcc00', // výrazná žlutá â next year plyn 2444 eua: '#3B5BC6', // emisnà povolenky EUA (modrá) 2445 brent: '#0d9488', // ropa Brent (teal) 2446 wti: '#8b5cf6' // ropa WTI (violet) 2447 }, 2448 dark: { 2449 el26: '#fca5a5', // svÄtle růžová 2450 el27: '#ef4444', 2451 el28: '#f87171', 2452 el29: '#fda4af', 2453 el_nextyear: '#ff5252', // výrazná Äervená â next year elektÅina (dark) 2454 gasTTF: '#fbbf24', 2455 gas27: '#fcd34d', 2456 gas28: '#fde047', 2457 gas29: '#fef08a', 2458 gas_nextyear: '#ffe14d', // výrazná žlutá â next year plyn (dark) 2459 eua: '#7C9BF0', // emisnà povolenky EUA (dark) 2460 brent: '#2dd4bf', // ropa Brent (dark) 2461 wti: '#c4b5fd' // ropa WTI (dark) 2462 } 2463}; 2464function getSeriesColor(id) { 2465 const theme = document.documentElement.getAttribute('data-theme') || 'light'; 2466 return (COLOR_PALETTE[theme] || COLOR_PALETTE.light)[id]; 2467} 2468const SERIES_DEF = [ 2469 { id: 'el_nextyear', key: 'el_nextyear', label: 'ElektÅina Next year' }, 2470 { id: 'el26', key: 'el26', label: 'ElektÅina Cal-26' }, 2471 { id: 'el27', key: 'el27', label: 'ElektÅina Cal-27' }, 2472 { id: 'el28', key: 'el28', label: 'ElektÅina Cal-28' }, 2473 { id: 'el29', key: 'el29', label: 'ElektÅina Cal-29' }, 2474 { id: 'gas_nextyear', key: 'gas_nextyear', label: 'Plyn Next year' }, 2475 { id: 'gasTTF', key: 'gasTTF', label: 'Plyn TTF front-month' }, 2476 { id: 'gas27', key: 'gas27', label: 'Plyn CZ Cal-27' }, 2477 { id: 'gas28', key: 'gas28', label: 'Plyn CZ Cal-28' }, 2478 { id: 'gas29', key: 'gas29', label: 'Plyn CZ Cal-29' } 2479]; 2480 2481function buildDatasetsForRange(range, chartKey) { 2482 const hiddenMap = (chartKey && priceChartState[chartKey] && priceChartState[chartKey].hidden) || {}; 2483 return SERIES_DEF.map(s => { 2484 const hidden = !!hiddenMap[s.id]; 2485 const pts = filterSeriesByRange(s.id, range); 2486 return { 2487 label: s.label, 2488 data: pts.map(p => ({ x: new Date(p.date).getTime(), y: cvDate(p.value, p.date, chartKey) })), 2489 borderColor: getSeriesColor(s.id), 2490 backgroundColor: getSeriesColor(s.id) + '14', 2491 pointRadius: 0, 2492 pointHoverRadius: 6, 2493 pointHoverBorderWidth: 2, 2494 borderWidth: 2, 2495 borderDash: s.id === 'gasTTF' ? [6, 4] : [], 2496 tension: 0.05, 2497 hidden: hidden, 2498 spanGaps: true, 2499 showLine: true, 2500 parsing: false 2501 }; 2502 }); 2503} 2504 2505function buildDatasetsForDates(fromStr, toStr, chartKey) { 2506 const hiddenMap = (chartKey && priceChartState[chartKey] && priceChartState[chartKey].hidden) || {}; 2507 return SERIES_DEF.map(s => { 2508 const hidden = !!hiddenMap[s.id]; 2509 const pts = filterSeriesByDates(s.id, fromStr, toStr); 2510 return { 2511 label: s.label, 2512 data: pts.map(p => ({ x: new Date(p.date).getTime(), y: cvDate(p.value, p.date, chartKey) })),
2513 borderColor: getSeriesColor(s.id), 2514 backgroundColor: getSeriesColor(s.id) + '14', 2515 pointRadius: 0, 2516 pointHoverRadius: 6, 2517 pointHoverBorderWidth: 2, 2518 borderWidth: 2, 2519 borderDash: s.id === 'gasTTF' ? [6, 4] : [], 2520 tension: 0.05, 2521 hidden: hidden, 2522 spanGaps: true, 2523 showLine: true, 2524 parsing: false 2525 }; 2526 }); 2527} 2528 2529function priceChartCommonOptions(chartKey) { 2530 const yZero = !!(chartKey && priceChartState[chartKey] && priceChartState[chartKey].yZero); 2531 const muted = getCssVar('--text-muted') || '#71717a'; 2532 const strong = getCssVar('--text-strong') || '#09090b'; 2533 const border = getCssVar('--border') || '#e4e4e7'; 2534 const bgCard = getCssVar('--bg-card') || '#fff'; 2535 return { 2536 responsive: true, 2537 maintainAspectRatio: false, 2538 interaction: { mode: 'nearest', intersect: false }, 2539 plugins: { 2540 // VestavÄná legenda vypnuta â použÃváme vlastnà HTML legendu (elektÅina / plyn na samostatných ÅádcÃch). 2541 legend: { display: false }, 2542 tooltip: { 2543 backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, 2544 borderColor: border, padding: 12, cornerRadius: 8, displayColors: true, boxPadding: 4, 2545 callbacks: { 2546 title: function(items) { 2547 if (!items.length) return ''; 2548 return new Date(items[0].parsed.x).toLocaleDateString('cs-CZ'); 2549 }, 2550 label: function(ctx) { 2551 const v = ctx.parsed.y; 2552 return ctx.dataset.label + ': ' + (v == null ? 'â' : v.toFixed(2).replace('.', ',') + ' ' + ccyUnit(chartKey)); 2553 } 2554 } 2555 } 2556 }, 2557 scales: { 2558 y: { 2559 type: 'linear', position: 'left', beginAtZero: yZero, 2560 title: { display: true, text: ccyUnit(chartKey), color: muted, font: {size: 11} }, 2561 grid: { color: border, lineWidth: 1 }, ticks: { color: muted, font: {size: 11} } 2562 }, 2563 x: { 2564 type: 'linear', 2565 bounds: 'data', 2566 afterDataLimits: captureXDataLimits, afterBuildTicks: calendarXTicks, 2567 ticks: { 2568 color: muted, autoSkip: false, maxRotation: 0, font: {size: 11}, 2569 callback: adaptiveXTick 2570 }, 2571 grid: { display: false }, 2572 title: { display: false } 2573 } 2574 } 2575 }; 2576} 2577 2578// Pomocná funkce â zjistÃ, ke kterému grafu daný element patÅà 2579function chartKeyFromEl(el) { 2580 const section = el?.closest('section[data-chart]'); 2581 return section?.dataset?.chart || 'el'; 2582} 2583function canvasIdFor(key) { return key === 'gas' ? 'price-chart-gas' : 'price-chart-el'; } 2584 2585function togglePriceYZero(chartKey, btn) { 2586 if (!priceChartState[chartKey]) return; 2587 priceChartState[chartKey].yZero = !priceChartState[chartKey].yZero; 2588 if (btn) { 2589 btn.classList.toggle('active', priceChartState[chartKey].yZero); 2590 btn.textContent = priceChartState[chartKey].yZero ? 'Adaptivnà Y' : 'ZaÄÃt od 0'; 2591 } 2592 // Pokud je aktivnà vlastnà rozsah dat, použij applyCustomRange; jinak drawPriceChart 2593 const section = document.querySelector('section[data-chart="' + chartKey + '"]'); 2594 const customFrom = section ? section.querySelector('input[id^="date-from-"]') : null; 2595 const customTo = section ? section.querySelector('input[id^="date-to-"]') : null; 2596 const isCustomActive = section && !section.querySelector('.range-controls [data-range].active'); 2597 if (isCustomActive && customFrom && customFrom.value && customTo && customTo.value) { 2598 applyCustomRange(customFrom); 2599 } else { 2600 drawPriceChart(chartKey, priceChartState[chartKey].range || '1m'); 2601 } 2602} 2603 2604// ===== MÄna EUR â CZK: pÅepoÄet dennÃm referenÄnÃm kurzem ECB ===== 2605window.__CCY = window.__CCY || {}; 2606function ccyOf(k){ return window.__CCY[k] || 'EUR'; } 2607function ccyUnit(k){ return ccyOf(k) === 'CZK' ? 'CZK/MWh' : 'EUR/MWh'; } 2608function ccySym(k){ return ccyOf(k) === 'CZK' ? 'KÄ' : 'â¬'; } 2609function _czk(k){ return ccyOf(k) === 'CZK'; } 2610function fxOnMs(ms){ 2611 const a = window.FX_DAILY; 2612 if (!a || !a.length) return null; 2613 let i = Math.floor((ms - window.FX_BASE_MS) / 86400000); 2614 if (i < 0) i = 0; if (i >= a.length) i = a.length - 1; 2615 return a[i]; 2616}
2617function fxOnDate(s){ return fxOnMs(new Date(s + 'T12:00:00Z').getTime()); } 2618function fxLatest(){ const a = window.FX_DAILY; return (a && a.length) ? a[a.length - 1] : null; } 2619function usdCzkOnMs(ms){ const a = window.FX_USD_DAILY; if (!a || !a.length) return null; let i = Math.floor((ms - window.FX_USD_BASE_MS) / 86400000); if (i < 0) i = 0; if (i >= a.length) i = a.length - 1; return a[i]; } 2620function usdCzkOn(ds){ return usdCzkOnMs(new Date(ds + 'T12:00:00Z').getTime()); } 2621function usdCzkLatest(){ const a = window.FX_USD_DAILY; return (a && a.length) ? a[a.length - 1] : null; } 2622function fxYearAvg(y){ const m = window.FX_YEAR_AVG; return (m && m[y]) ? m[y] : fxLatest(); } 2623function fxYearKeyRate(yk){ 2624 if (yk === '3yr') { const rs = [2023,2024,2025].map(fxYearAvg).filter(Boolean); return rs.length ? rs.reduce((a,b)=>a+b,0)/rs.length : fxLatest(); } 2625 return fxYearAvg(parseInt(yk, 10)); 2626} 2627// PÅevody EUR â zobrazovaná mÄna grafu k (EUR ponecháno; CZK = EUR à kurz daného dne / roku) 2628function cvMs(eur, ms, k){ if (eur == null || !isFinite(eur)) return eur; if (!_czk(k)) return eur; const r = fxOnMs(ms); return r ? eur * r : eur; } 2629function cvDate(eur, ds, k){ if (eur == null || !isFinite(eur)) return eur; if (!_czk(k)) return eur; const r = fxOnDate(ds); return r ? eur * r : eur; } 2630function cvRate(eur, rate, k){ if (eur == null || !isFinite(eur)) return eur; if (!_czk(k)) return eur; return rate ? eur * rate : eur; } 2631function toggleCcy(k, btn){ 2632 window.__CCY[k] = ccyOf(k) === 'EUR' ? 'CZK' : 'EUR'; 2633 document.querySelectorAll('[data-ccy-for="' + k + '"]').forEach(b => { 2634 b.textContent = ccyOf(k) === 'EUR' ? 'KÄ' : 'â¬'; 2635 b.classList.toggle('active', ccyOf(k) === 'CZK'); 2636 }); 2637 redrawChartCcy(k); 2638} 2639function redrawChartCcy(k){ 2640 if (k === 'el' || k === 'gas') return redrawPriceChart(k); 2641 if (k === 'ote') return drawOteChart(); 2642 if (k === 'spot5') return redrawSpot5(); 2643 if (k === 'fwd') return drawForwardSpotChart(); 2644 if (k === 'hour') return drawSpotHourChart(); 2645 if (k === 'dow') return drawSpotDowChart(); 2646} 2647function redrawPriceChart(k){ 2648 const section = document.querySelector('section[data-chart="' + k + '"]'); 2649 const cf = section ? section.querySelector('input[id^="date-from-"]') : null; 2650 const ct = section ? section.querySelector('input[id^="date-to-"]') : null; 2651 const isCustom = section && !section.querySelector('.range-controls [data-range].active'); 2652 if (isCustom && cf && cf.value && ct && ct.value) applyCustomRange(cf); 2653 else drawPriceChart(k, (priceChartState[k] && priceChartState[k].range) || '1m'); 2654} 2655function redrawSpot5(){ 2656 if (__spotRange === 'compare') return drawSpotCompareChart(); 2657 if (__spotRange === 'custom') return applySpotCustomRange(); 2658 return drawSpotHourlyChart(); 2659} 2660 2661function drawPriceChart(key, range) { 2662 key = key || 'el'; 2663 priceChartState[key].range = range; 2664 if (priceCharts[key]) priceCharts[key].destroy(); 2665 const ctx = document.getElementById(canvasIdFor(key)).getContext('2d'); 2666 priceCharts[key] = new Chart(ctx, { 2667 type: 'line', 2668 data: { datasets: buildDatasetsForRange(range, key) }, 2669 options: priceChartCommonOptions(key), 2670 plugins: [nextYearGlowPlugin] 2671 }); 2672 // legacy alias (other code mohlo použÃvat priceChart) 2673 if (key === 'el') priceChart = priceCharts.el; 2674 renderPriceLegend(key); 2675} 2676 2677// ===== Vlastnà HTML legenda grafů 3A/3B ===== 2678// 3A (elektÅina): Åádek 1 = elektÅina, Åádek 2 = plyn. 3B (plyn): pÅesnÄ naopak. 2679function renderPriceLegend(key) { 2680 const cont = document.getElementById('price-legend-' + key); 2681 if (!cont) return; 2682 const elSeries = SERIES_DEF.filter(s => s.id.indexOf('el') === 0); 2683 const gasSeries = SERIES_DEF.filter(s => s.id.indexOf('gas') === 0); 2684 const rows = (key === 'gas') ? [gasSeries, elSeries] : [elSeries, gasSeries]; 2685 const hiddenMap = (priceChartState[key] && priceChartState[key].hidden) || {}; 2686 cont.innerHTML = rows.map(function (group) { 2687 const items = group.map(function (s) { 2688 const hidden = !!hiddenMap[s.id]; 2689 const color = getSeriesColor(s.id); 2690 return '<span class="pl-item' + (hidden ? ' pl-hidden' : '') + '" onclick="togglePriceSeries(\'' + key + '\',\'' + s.id + '\')">' + 2691 '<span class="pl-swatch" style="background:' + color + '"></span>' + s.label + '</span>'; 2692 }).join(''); 2693 return '<div class="price-legend-row">' + items + '</div>'; 2694 }).join(''); 2695} 2696 2697function togglePriceSeries(key, id) { 2698 const chart = priceCharts[key]; 2699 if (!chart) return; 2700 const idx = SERIES_DEF.findIndex(s => s.id === id); 2701 if (idx < 0) return; 2702 const next = !priceChartState[key].hidden[id]; 2703 priceChartState[key].hidden[id] = next; 2704 chart.setDatasetVisibility(idx, !next); // visible = !hidden
2705 chart.update(); 2706 renderPriceLegend(key); 2707} 2708 2709function toggleSeries(btnOrId, maybeSeriesId) { 2710 // Kompatibilita: staršà volánà toggleSeries('el27') i nové toggleSeries(this, 'el27') 2711 let btn, seriesId, key; 2712 if (typeof btnOrId === 'string') { 2713 seriesId = btnOrId; 2714 btn = document.querySelector('[data-series="' + seriesId + '"]'); 2715 key = chartKeyFromEl(btn); 2716 } else { 2717 btn = btnOrId; 2718 seriesId = maybeSeriesId; 2719 key = chartKeyFromEl(btn); 2720 } 2721 btn.classList.toggle('active'); 2722 const idx = SERIES_DEF.findIndex(s => s.id === seriesId); 2723 const chart = priceCharts[key]; 2724 if (chart && idx >= 0) { 2725 const meta = chart.getDatasetMeta(idx); 2726 meta.hidden = !btn.classList.contains('active'); 2727 chart.update(); 2728 } 2729} 2730 2731function setPriceRange(btnOrRange, maybeRange) { 2732 let btn, range, key; 2733 if (typeof btnOrRange === 'string') { 2734 range = btnOrRange; 2735 btn = document.querySelector(`[data-range="${range}"]`); 2736 key = chartKeyFromEl(btn); 2737 } else { 2738 btn = btnOrRange; 2739 range = maybeRange; 2740 key = chartKeyFromEl(btn); 2741 } 2742 // Aktivace tlaÄÃtka jen v dané sekci 2743 const section = btn.closest('section[data-chart]'); 2744 section.querySelectorAll('.range-controls [data-range]').forEach(b => { 2745 b.classList.toggle('active', b.dataset.range === range); 2746 }); 2747 const fromInp = section.querySelector('input[id^="date-from-"]'); 2748 const toInp = section.querySelector('input[id^="date-to-"]'); 2749 // NaplÅ OBÄ okýnka skuteÄným rozsahem (Od i Do), aÅ¥ jsou vždy vyplnÄná a reagujà na tlaÄÃtka. 2750 const probeSid = key === 'gas' ? 'gas27' : 'el27'; 2751 const allPts = getSeriesPoints(probeSid); 2752 const firstDate = allPts.length ? allPts[0].date : null; 2753 const lastDate = allPts.length ? allPts[allPts.length-1].date : null; 2754 const rs = rangeStart(range); 2755 const pad = n => String(n).padStart(2, '0'); 2756 let fromStr = rs.getFullYear() + '-' + pad(rs.getMonth() + 1) + '-' + pad(rs.getDate()); 2757 if (firstDate && fromStr < firstDate) fromStr = firstDate; // neukazuj datum pÅed zaÄátkem dat 2758 if (fromInp) fromInp.value = fromStr; 2759 if (toInp && lastDate) toInp.value = lastDate; 2760 drawPriceChart(key, range); 2761} 2762 2763function applyCustomRange(inp) { 2764 const section = inp.closest('section[data-chart]'); 2765 const key = section.dataset.chart; 2766 const from = section.querySelector('input[id^="date-from-"]').value; 2767 const to = section.querySelector('input[id^="date-to-"]').value; 2768 if (!from || !to) return; 2769 section.querySelectorAll('.range-controls [data-range]').forEach(b => b.classList.remove('active')); 2770 if (priceCharts[key]) priceCharts[key].destroy(); 2771 const ctx = document.getElementById(canvasIdFor(key)).getContext('2d'); 2772 priceCharts[key] = new Chart(ctx, { 2773 type: 'line', 2774 data: { datasets: buildDatasetsForDates(from, to, key) }, 2775 options: priceChartCommonOptions(key), 2776 plugins: [nextYearGlowPlugin] 2777 }); 2778 if (key === 'el') priceChart = priceCharts.el; 2779 renderPriceLegend(key); 2780} 2781 2782// Barvy roků sjednocené se sekcà 5 (SPOT_YEAR_COLORS) 2783const YEAR_COLORS = { 2784 '2020': '#737373', // Å¡edá 2785 '2021': '#8b5cf6', // fialová 2786 '2022': '#3b82f6', // modrá 2787 '2023': '#06b6d4', // tyrkysová 2788 '2024': '#10b981', // zelená 2789 '2025': '#f59e0b', // oranžová 2790 '2026': '#dc2626' // Äervená 2791}; 2792 2793function drawStorageHistoryChart() { 2794 const muted = getCssVar('--text-muted') || '#71717a'; 2795 const border = getCssVar('--border') || '#e4e4e7'; 2796 const strong = getCssVar('--text-strong') || '#09090b'; 2797 const bgCard = getCssVar('--bg-card') || '#fff'; 2798 const datasets = Object.keys(storageHistory).map(year => ({ 2799 label: year, 2800 data: storageHistory[year].map(p => ({ x: p.week, y: p.value })), 2801 borderColor: paletteColor(YEAR_COLORS[year]), 2802 backgroundColor: paletteColor(YEAR_COLORS[year]) + '14', 2803 pointRadius: 0, 2804 pointHoverRadius: 4, 2805 borderWidth: year === '2026' ? 2.5 : 1.8, 2806 tension: 0.35, 2807 hidden: false 2808 })); 2809 // PrůmÄrná kÅivka pÅes kompletnà roky 2020â2025 (ÄárkovanÄ, neutrálnà barva). 2810 // PÅidává se NA KONEC datasets â legend-toggle dle indexu roků zůstává funkÄnÃ. 2811 (function addAvgDataset() { 2812 const fullYears = Object.keys(storageHistory).filter(y => y >= '2020' && y <= '2025'); 2813 if (!fullYears.length) return; 2814 const sum = {}, n = {};
2815 fullYears.forEach(y => storageHistory[y].forEach(p => { 2816 const w = Math.round(p.week); 2817 sum[w] = (sum[w] || 0) + p.value; n[w] = (n[w] || 0) + 1; 2818 })); 2819 const avgData = Object.keys(sum).map(Number).sort((a, b) => a - b) 2820 .map(w => ({ x: w, y: Math.round(sum[w] / n[w] * 10) / 10 })); 2821 datasets.push({ 2822 label: 'PrůmÄr 2020â2025', data: avgData, 2823 borderColor: muted, backgroundColor: 'transparent', 2824 borderDash: [6, 4], pointRadius: 0, pointHoverRadius: 3, 2825 borderWidth: 2, tension: 0.35, fill: false 2826 }); 2827 })(); 2828 if (storageHistoryChart) storageHistoryChart.destroy(); 2829 const _shcEl = document.getElementById('storage-history-chart'); 2830 if (!_shcEl) return; 2831 const ctx = _shcEl.getContext('2d'); 2832 storageHistoryChart = new Chart(ctx, { 2833 type: 'line', 2834 data: { datasets: datasets }, 2835 options: { 2836 responsive: true, maintainAspectRatio: false, 2837 interaction: { mode: 'nearest', intersect: false }, 2838 plugins: { 2839 legend: { display: true, position: 'top', labels: { color: muted, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12, boxHeight: 8, padding: 12, font: {size: 12, weight: '500'} } }, 2840 tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8, 2841 callbacks: { title: items => 'Týden ' + Math.round(items[0].parsed.x), 2842 label: item => item.dataset.label + ': ' + item.parsed.y.toFixed(1).replace('.', ',') + ' %' } } 2843 }, 2844 scales: { 2845 x: { 2846 type: 'linear', min: 1, max: 53.2, 2847 title: { display: true, text: 'KalendáÅnà týden', color: muted, font: {size: 11} }, 2848 ticks: { stepSize: 4, color: muted, font: {size: 11} }, 2849 grid: { display: false } 2850 }, 2851 y: { 2852 min: 0, max: 100, 2853 title: { display: true, text: '% zaplnÄnà (working gas volume)', color: muted, font: {size: 11} }, 2854 grid: { color: border, lineWidth: 1 }, 2855 ticks: { color: muted, font: {size: 11} } 2856 } 2857 } 2858 } 2859 }); 2860} 2861 2862// ===== Ceny tepla v krajÃch ÄR â vážené průmÄry (CZK/GJ vÄ. DPH) ===== 2863// VypoÄÃtáno váženým průmÄrem (cena à dodané GJ) pÅes vÅ¡echny cenové lokality v kraji. 2864// Extrahováno z PDF pÅes pdf.js + parser SPZ-kódů (A/B/C/E/H/J/K/L/M/P/S/T/U/Z). 2865// Zdroj 2024: "Výsledné ceny tepelné energie v ÄR 2024" (51 str., 3 557 lokalit). 2866// Zdroj 2025: "PÅedbÄžné ceny tepelné energie v ÄR k 1. 1. 2025" (56 str., 3 700 lokalit). 2867const heatData = {source: "ERÃ", defaultYear: "2025", byYear: {}}; 2868let __heatYear = heatData.defaultYear; 2869 2870let heatChart = null; 2871function setHeatYear(year) { 2872 __heatYear = year; 2873 document.querySelectorAll('.range-controls [data-heat-year]').forEach(b => { 2874 b.classList.toggle('active', b.dataset.heatYear === year); 2875 }); 2876 drawHeatChart(); 2877} 2878// ===== Sekce 17: ZEVO ÄR ===== 2879let zevoChart = null; 2880window.__zevoYear = 2024; 2881window.__zevoMetric = 'waste'; 2882window.__zevoStatus = 'all'; 2883function setZevoYear(y) { 2884 window.__zevoYear = y; 2885 document.querySelectorAll('[data-zevo-year]').forEach(b => b.classList.toggle('active', parseInt(b.dataset.zevoYear) === y)); 2886 drawZevoChart(); 2887} 2888function setZevoMetric(m) { 2889 window.__zevoMetric = m; 2890 document.querySelectorAll('[data-zevo-metric]').forEach(b => b.classList.toggle('active', b.dataset.zevoMetric === m)); 2891 drawZevoChart(); 2892} 2893function setZevoStatus(s) { 2894 window.__zevoStatus = s; 2895 document.querySelectorAll('[data-zevo-status]').forEach(b => b.classList.toggle('active', b.dataset.zevoStatus === s)); 2896 drawZevoChart(); 2897} 2898function drawZevoChart() { 2899 if (!window.zevoData) return; 2900 const muted = getCssVar('--text-muted') || '#71717a'; 2901 const border = getCssVar('--border') || '#e4e4e7'; 2902 const strong = getCssVar('--text-strong') || '#09090b'; 2903 const bgCard = getCssVar('--bg-card') || '#fff'; 2904 const accent = getCssVar('--accent') || '#dc2626'; 2905 // Pro plánované/rozestavÄné použÃváme âsnÃmek" z year=2024 Åádku (status, expected_year, kapacita) 2906 // ale data o produkci se ukazujà jen pro Åádky odpovÃdajÃcà vybranému roku. 2907 const statusFilter = window.__zevoStatus; 2908 // Pro každé ZEVO: vezmi Åádek pro vybraný rok, pokud neexistuje a statusâ operational, vezmi 2024 snÃmek 2909 const byId = {}; 2910 window.zevoData.forEach(r => { 2911 if (!byId[r.zevo_id]) byId[r.zevo_id] = {}; 2912 byId[r.zevo_id][r.year] = r; 2913 }); 2914 const rows = Object.values(byId).map(yMap => { 2915 const yearRow = yMap[window.__zevoYear]; 2916 if (yearRow) return yearRow; 2917 // pro neoperaÄnà ZEVO může chybÄt Åádek pro 2023 nebo 2025 â použij 2024 jako fallback 2918 return yMap[2024] || Object.values(yMap)[0]; 2919 }).filter(r => { 2920 if (statusFilter === 'all') return true; 2921 return r.status === statusFilter; 2922 }); 2923 // Sort: operational prvnÃ, pak under_construction, pak planned; v rámci skupiny podle kapacity desc 2924 const statusOrder = { operational: 0, under_construction: 1, planned: 2 }; 2925 rows.sort((a, b) => { 2926 const sa = statusOrder[a.status] ?? 99, sb = statusOrder[b.status] ?? 99; 2927 if (sa !== sb) return sa - sb; 2928 return (parseFloat(b.capacity_t_year) || 0) - (parseFloat(a.capacity_t_year) || 0); 2929 }); 2930 // Render tabulka pro daný rok 2931 const tbody = document.getElementById('zevo-tbody'); 2932 if (tbody) { 2933 const fmt = n => n === null || n === undefined ? 'â' : Math.round(parseFloat(n)).toLocaleStr
2933ing('cs-CZ'); 2934 const statusBadge = r => { 2935 if (r.status === 'operational') return '<span style="background:#dcfce7;color:#166534;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600">v provozu</span>'; 2936 if (r.status === 'under_construction') return `<span style="background:#fef3c7;color:#92400e;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600">ve výstavbÄ${r.expected_year ? ' â ' + r.expected_year : ''}</span>`; 2937 if (r.status === 'planned') return `<span style="background:#e0e7ff;color:#3730a3;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600">plán${r.expected_year ? ' ' + r.expected_year : ''}</span>`; 2938 return r.status; 2939 }; 2940 // V neoperaÄnÃch ÅádcÃch nemá smysl zobrazovat produkÄnà data pro vybraný rok (jeÅ¡tÄ nejedou) 2941 const dataCells = r => { 2942 if (r.status !== 'operational' || r.year !== window.__zevoYear) { 2943 return `<td class="num">${fmt(r.capacity_t_year)}</td><td class="num" colspan="3" style="color:var(--text-muted);font-style:italic">${r.status === 'operational' ? 'data za ' + window.__zevoYear + ' nedostupná' : 'zatÃm neprovozuje'}</td>`; 2944 } 2945 return `<td class="num">${fmt(r.capacity_t_year)}</td><td class="num">${fmt(r.waste_tonnes)}</td><td class="num">${fmt(r.electricity_mwh)}</td><td class="num">${fmt(r.heat_tj)}</td>`; 2946 }; 2947 tbody.innerHTML = rows.map(r => `<tr> 2948 <td><strong>${r.zevo_name}</strong><br><span style="color:var(--text-muted);font-size:11px">${r.city || ''}</span></td> 2949 <td>${r.operator || 'â'}</td> 2950 <td class="num">${statusBadge(r)}</td> 2951 ${dataCells(r)} 2952 </tr>`).join(''); 2953 } 2954 // Render bar chart pro vybranou metriku â jen operational ZEVO s daty pro daný rok 2955 const metric = window.__zevoMetric; 2956 const colMap = { waste: 'waste_tonnes', electricity: 'electricity_mwh', heat: 'heat_tj' }; 2957 const labelMap = { waste: 'Tunáž odpadu (t)', electricity: 'ElektÅina (MWh)', heat: 'Teplo (TJ)' }; 2958 const colorMap = { waste: '#71717a', electricity: '#dc2626', heat: '#f59e0b' }; 2959 const col = colMap[metric]; 2960 const chartRows = rows.filter(r => r.status === 'operational' && r.year === window.__zevoYear && r[col] !== null && r[col] !== undefined); 2961 const labels = chartRows.map(r => r.zevo_name); 2962 const data = chartRows.map(r => parseFloat(r[col]) || 0); 2963 const ctx = document.getElementById('zevo-chart').getContext('2d'); 2964 if (zevoChart) zevoChart.destroy(); 2965 zevoChart = new Chart(ctx, { 2966 type: 'bar', 2967 data: { labels, datasets: [{ 2968 label: labelMap[metric] + ' (' + window.__zevoYear + ')', 2969 data, 2970 backgroundColor: colorMap[metric], 2971 borderRadius: 6, 2972 borderWidth: 0 2973 }] }, 2974 options: { 2975 responsive: true, maintainAspectRatio: false, 2976 plugins: { 2977 legend: { display: false }, 2978 tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8, 2979 callbacks: { label: ctx => Math.round(ctx.parsed.y).toLocaleString('cs-CZ') + ' ' + labelMap[metric].match(/\(([^)]+)\)/)[1] } } 2980 }, 2981 scales: { 2982 y: { beginAtZero: true, title: { display: true, text: labelMap[metric], color: muted }, grid: { color: border }, ticks: { color: muted, callback: v => v.toLocaleString('cs-CZ') } }, 2983 x: { grid: { display: false }, ticks: { color: muted } } 2984 } 2985 } 2986 }); 2987} 2988 2989function drawHeatChart() { 2990 const muted = getCssVar('--text-muted') || '#71717a'; 2991 const border = getCssVar('--border') || '#e4e4e7'; 2992 const strong = getCssVar('--text-strong') || '#09090b'; 2993 const bgCard = getCssVar('--bg-card') || '#fff'; 2994 const accent = getCssVar('--accent') || '#dc2626'; 2995 const year = __heatYear; 2996 const prevYear = String(parseInt(year) - 1); 2997 const values = heatData.byYear[year] || []; 2998 const prevValues = heatData.byYear[prevYear] || []; 2999 // Sort by price descending 3000 const sorted = [...values].sort((a, b) => b.price - a.price); 3001 const labels = sorted.map(d => d.kraj); 3002 const data = sorted.map(d => d.price); 3003 // SpoÄÃtej meziroÄnà zmÄnu 3004 sorted.forEach(s => { 3005 const prev = prevValues.find(p => p.kraj === s.kraj); 3006 s.prevPrice = prev ? prev.price : null; 3007 s.yoyDelta = prev ? Math.round((s.price - prev.price) * 10) / 10 : null; 3008 s.yoyPct = prev ? Math.round(((s.price - prev.price) / prev.price) * 1000) / 10 : null; 3009 }); 3010 // Barvy: gradient od max po min, null = Å¡edá 3011 const max = Math.max(...data.filter(v => v !== null)); 3012 const min = Math.min(...data.filter(v => v !== null)); 3013 const colors = sorted.map(d => { 3014 if (d.price === null) return getCssVar('--bg-subtle') || '#e4e4e7'; 3015 const t = (d.price - min) / (max - min); 3016 // Interpoluj mezi delta-down (cool) a accent (warm) 3017 return interpolateColor(getCssVar('--delta-down') || '#047857', accent, t); 3018 }); 3019 if (heatChart) heatChart.destroy(); 3020 const ctx = document.getElementById('heat-chart').getContext('2d'); 3021 heatChart = new Chart(ctx, { 3022 type: 'bar', 3023 data: { 3024 labels: labels, 3025 datasets: [{ 3026 label: 'Cena tepla (CZK/GJ vÄ. DPH)', 3027 data: data, 3028 backgroundColor: colors, 3029 borderColor: colors, 3030 borderWidth: 0, 3031 borderRadius: 6, 3032 borderSkipped: false 3033 }] 3034 }, 3035 options: { 3036 indexAxis: 'y', 3037 responsive: true, 3038 maintainAspectRatio: false, 3039 plugins: { 3040 legend: { display: false }, 3041 tooltip: { 3042 backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, 3043 padding: 12, cornerRadius: 8, displayColors: false, 3044 callbacks: { 3045 label: (ctx) => { 3046 const v = ctx.parsed.x; 3047 if (v === null || v === undefined) return 'Data nedostupná';
3048 const row = sorted[ctx.dataIndex]; 3049 const lines = [v.toLocaleString('cs-CZ') + ' CZK/GJ (' + year + ')']; 3050 if (row.yoyDelta !== null && row.yoyDelta !== undefined) { 3051 const sign = row.yoyDelta >= 0 ? '+' : ''; 3052 lines.push('vs ' + prevYear + ': ' + sign + row.yoyDelta + ' (' + sign + row.yoyPct + ' %)'); 3053 } 3054 if (row.locality) lines.push(row.locality + ' lokalit v kraji'); 3055 if (row.gj) lines.push((row.gj / 1000).toLocaleString('cs-CZ', {maximumFractionDigits: 0}) + ' tis. GJ celkem'); 3056 return lines; 3057 } 3058 } 3059 } 3060 }, 3061 scales: { 3062 x: { 3063 title: { display: true, text: 'CZK/GJ vÄ. DPH', color: muted, font: {size: 11} }, 3064 grid: { color: border, lineWidth: 1 }, 3065 ticks: { color: muted, font: {size: 11}, callback: v => v.toLocaleString('cs-CZ') } 3066 }, 3067 y: { 3068 ticks: { color: muted, font: {size: 12, weight: '500'} }, 3069 grid: { display: false } 3070 } 3071 } 3072 } 3073 }); 3074} 3075 3076function interpolateColor(c1, c2, t) { 3077 const parse = (c) => { 3078 const h = c.replace('#',''); 3079 return [parseInt(h.slice(0,2),16), parseInt(h.slice(2,4),16), parseInt(h.slice(4,6),16)]; 3080 }; 3081 const a = parse(c1), b = parse(c2); 3082 const r = Math.round(a[0] + (b[0]-a[0])*t); 3083 const g = Math.round(a[1] + (b[1]-a[1])*t); 3084 const bl = Math.round(a[2] + (b[2]-a[2])*t); 3085 return `rgb(${r},${g},${bl})`; 3086} 3087 3088// ===== Spot ceny grafy ===== 3089let spotHourlyChart = null; 3090let spotHourChart = null; 3091let spotDowChart = null; 3092let __spotRange = '1y'; 3093let __spotHourYear = '3yr'; 3094let __spotDowYear = '3yr'; 3095 3096const DAY_NAMES = ['PondÄlÃ','Ãterý','StÅeda','Ätvrtek','Pátek','Sobota','NedÄle']; 3097 3098// PoziÄnÄ, ne podle timestamp â Excel data majà DST gapy a nepokrývajà kontinuálnÄ každou hodinu 3099function spotRangeStartIdx(range, totalHours) { 3100 const hoursMap = { '7d': 7*24, '30d': 30*24, '90d': 90*24, '1y': 365*24, '3y': 3*365*24 }; 3101 if (range === 'all') return 0; 3102 const n = hoursMap[range]; 3103 return n ? Math.max(0, totalHours - n) : 0; 3104} 3105// Proporcionálnà mapovánà idx â datum (firstTs..lastTs linearly spread pÅes celé pole) 3106function spotIdxToMs(idx, firstTsMs, lastTsMs, totalHours) { 3107 if (totalHours <= 1) return firstTsMs; 3108 return firstTsMs + idx * (lastTsMs - firstTsMs) / (totalHours - 1); 3109} 3110function spotMsToIdx(ms, firstTsMs, lastTsMs, totalHours) { 3111 if (lastTsMs <= firstTsMs) return 0; 3112 return Math.round((ms - firstTsMs) / (lastTsMs - firstTsMs) * (totalHours - 1)); 3113} 3114 3115// Lokálnà datum ms â 'YYYY-MM-DD' pro <input type=date> 3116function spotFmtInput(ms) { 3117 const d = new Date(ms); 3118 const p = n => String(n).padStart(2, '0'); 3119 return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()); 3120} 3121 3122function setSpotRange(range) { 3123 __spotRange = range; 3124 document.querySelectorAll('[data-spot-range]').forEach(b => b.classList.toggle('active', b.dataset.spotRange === range)); 3125 // Okýnka Od/Do se nevymazávajà â draw funkce je naplnà skuteÄným rozsahem (aÅ¥ jsou vždy bÃlá a vyplnÄná). 3126 // Zobraz/skryj toggles roků pro compare mode 3127 const toggles = document.getElementById('spot-year-toggles'); 3128 if (toggles) toggles.style.display = range === 'compare' ? '' : 'none'; 3129 if (range === 'compare') drawSpotCompareChart(); 3130 else drawSpotHourlyChart(); 3131} 3132 3133const SPOT_YEAR_COLORS = { 3134 '2020':'#737373', // Å¡edá 3135 '2021':'#8b5cf6', // fialová 3136 '2022':'#3b82f6', // modrá 3137 '2023':'#06b6d4', // tyrkysová 3138 '2024':'#10b981', // zelená 3139 '2025':'#f59e0b', // oranžová 3140 '2026':'#dc2626' // Äervená 3141}; 3142 3143function toggleSpotYear(year) { 3144 const btn = document.querySelector('[data-spot-year-toggle="' + year + '"]'); 3145 if (btn) btn.classList.toggle('active'); 3146 drawSpotCompareChart(); 3147} 3148 3149function drawSpotCompareChart() { 3150 if (!window.spotData) return; 3151 const muted = getCssVar('--text-muted') || '#71717a'; 3152 const border = getCssVar('--border') || '#e4e4e7'; 3153 const strong = getCssVar('--text-strong') || '#09090b'; 3154 const bgCard = getCssVar('--bg-card') || '#fff'; 3155 // Srovnánà let v HODINOVÃM rozliÅ¡enÃ: z vysokorozliÅ¡ovacà série (hodinová + 15min), 3156 // body zarovnané podle pozice v roce (frakÄnà den v roce), roky pÅekryté pÅes sebe. 3157 const series = window.spotData.series || []; 3158 if (!series.length) return;
3159 // Okýnka Od/Do naplÅ celým rozsahem dat (vždy vyplnÄná, bÃlá) 3160 const _fEl = document.getElementById('spot-from'), _tEl = document.getElementById('spot-to'); 3161 if (_fEl) _fEl.value = spotFmtInput(series[0].x); 3162 if (_tEl) _tEl.value = spotFmtInput(series[series.length - 1].x); 3163 const byYear = {}; 3164 const sumY = {}, cntY = {}; 3165 for (let i = 0; i < series.length; i++) { 3166 const p = series[i]; 3167 const d = new Date(p.x); 3168 const y = d.getFullYear(); 3169 const jan1 = new Date(y, 0, 1).getTime(); 3170 const doy = (p.x - jan1) / 86400000 + 1; // 1-based frakÄnà den v roce 3171 const yv = cvMs(p.y, p.x, 'spot5'); 3172 (byYear[y] = byYear[y] || []).push({ x: doy, y: yv }); 3173 if (isFinite(yv)) { sumY[y] = (sumY[y] || 0) + yv; cntY[y] = (cntY[y] || 0) + 1; } 3174 } 3175 const datasets = []; 3176 // Pole roků vzestupnÄ (kvůli legendÄ 2020â2026). Z-order ale ÅÃdà vlastnost `order`: 3177 // Chart.js kreslà dataset s NEJNIŽŠÃM `order` navrch â nejnovÄjšà rok dostane order 0 3178 // (popÅedÃ), nejstaršà nejvyššà order (pozadÃ). Hloubkový gradient: novÄjšà sytÄjÅ¡Ã+silnÄjÅ¡Ã. 3179 const yearsAsc = Object.keys(byYear).sort(); 3180 const n = yearsAsc.length; 3181 yearsAsc.forEach((year, idx) => { 3182 const depth = n > 1 ? idx / (n - 1) : 1; // 0 = nejstarÅ¡Ã, 1 = nejnovÄjšà 3183 const base = paletteColor(SPOT_YEAR_COLORS[year] || '#71717a'); 3184 const alpha = 0.40 + 0.60 * depth; // staršà = průhlednÄjšà (pozadÃ) 3185 const bw = 0.7 + 0.9 * depth; // novÄjšà = silnÄjšà Äára (popÅedÃ) 3186 datasets.push({ 3187 label: year, 3188 data: byYear[year], 3189 borderColor: hexToRgba(base, alpha), 3190 backgroundColor: 'transparent', 3191 pointRadius: 0, 3192 pointHoverRadius: 4, 3193 borderWidth: bw, 3194 tension: 0, 3195 hidden: false, 3196 spanGaps: false, 3197 parsing: false, 3198 order: (n - 1) - idx // nejnovÄjšà rok â nejnižšà order â kreslà se navrch (popÅedÃ) 3199 }); 3200 }); 3201 // Stats: roÄnà průmÄry (z hodinových dat) 3202 const yearAvgs = Object.keys(byYear).sort().map(y => ({ year: y, avg: cntY[y] ? sumY[y] / cntY[y] : 0 })); 3203 const statsEl = document.getElementById('spot-stats'); 3204 if (statsEl) statsEl.innerHTML = 'RoÄnà průmÄry (hodinové): ' + yearAvgs.map(s => `<strong>${s.year}</strong>: ${s.avg.toFixed(1)} ${ccySym('spot5')}`).join(' ⢠'); 3205 if (spotHourlyChart) spotHourlyChart.destroy(); 3206 const ctx = document.getElementById('spot-hourly-chart').getContext('2d'); 3207 // Month tick positions (day-of-year for 1st of each month, non-leap) 3208 const monthStarts = [1, 32, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335]; 3209 const monthNames = ['Led','Ãno','BÅe','Dub','KvÄ','Ävn','Ävc','Srp','ZáÅ','ÅÃj','Lis','Pro']; 3210 spotHourlyChart = new Chart(ctx, { 3211 type: 'line', 3212 data: { datasets: datasets }, 3213 options: { 3214 responsive: true, maintainAspectRatio: false, 3215 animation: false, 3216 interaction: { mode: 'nearest', intersect: false }, 3217 plugins: { 3218 legend: { display: true, position: 'top', labels: { color: muted, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12, padding: 12, font: {size: 12, weight: '500'} } }, 3219 tooltip: { 3220 backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8, 3221 callbacks: { 3222 title: function(items) { 3223 if (!items.length) return ''; 3224 // Datum se poÄÃtá z ROKU dané kÅivky â je to pÅesná inverze vzorce, kterým 3225 // se x stavÄlo (doy = (ts - 1.1. roku)/den + 1). DÅÃvÄjšà pevný základ 3226 // new Date(2024,0,dd) byl PÅESTUPNà rok, kdežto osa použÃvá nepÅestupné 3227 // monthStarts â od bÅezna se tooltip s osou rozcházel o den. 3228 const it = items[0]; 3229 const x = it.parsed.x; 3230 const rok = parseInt(it.dataset && it.dataset.label, 10); 3231 if (!isFinite(rok)) return ''; 3232 const ms = new Date(rok, 0, 1).getTime() + (x - 1) * 86400000; 3233 const d = new Date(Math.round(ms / 3600000) * 3600000); // zaokrouhli na celou hodinu 3234 return d.toLocaleDateString('cs-CZ', {day:'numeric', month:'long', year:'numeric'}) 3235 + ', ' + d.getHours() + 'h'; 3236 }, 3237 label: function(ctx) { return ctx.dataset.label + ': ' + ctx.parsed.y.toFixed(2) + ' ' + ccyUnit('spot5'); } 3238 } 3239 } 3240 }, 3241 scales: { 3242 y: { title: {display: true, text: ccyUnit('spot5') + ' (hodinová)', color: muted}, grid: {color: border}, ticks: {color: muted} }, 3243 x: { 3244 type: 'linear', min: 1, max: 367, 3245 title: {display: true, text: 'MÄsÃc', color: muted}, 3246 grid: {display: false}, 3247 ticks: { 3248 color: muted, 3249 callback: function(v) { 3250 const den = Math.round(v); 3251 const rozsah = (isFinite(this.max) && isFinite(this.min)) ? (this.max - this.min) : 366; 3252 if (rozsah > 120) { // celý rok â jen názvy mÄsÃců
3253 const idx = monthStarts.indexOf(den); 3254 return idx >= 0 ? monthNames[idx] : ''; 3255 } 3256 // po zoomu má âLed/Ãno" nulovou vypovÃdacà hodnotu â konkrétnà datum 3257 let m = 0; 3258 for (let i = 0; i < monthStarts.length; i++) if (monthStarts[i] <= den) m = i; 3259 return (den - monthStarts[m] + 1) + '. ' + (m + 1) + '.'; 3260 }, 3261 autoSkip: false, 3262 stepSize: 1 3263 }, 3264 afterBuildTicks: function(axis) { 3265 // Ticky se musà pÅepoÄÃtat podle AKTUÃLNÃHO rozsahu, jinak osa po zoomu 3266 // dál kreslà vÅ¡ech 12 mÄsÃců vÄetnÄ tÄch mimo výÅez. 3267 const min = axis.min, max = axis.max, rozsah = max - min; 3268 if (!isFinite(min) || !isFinite(max)) { axis.ticks = monthStarts.map(s => ({ value: s })); return; } 3269 if (rozsah > 120) { 3270 axis.ticks = monthStarts.filter(s => s >= min && s <= max).map(s => ({ value: s })); 3271 return; 3272 } 3273 const krok = rozsah > 60 ? 7 : rozsah > 30 ? 3 : rozsah > 12 ? 2 : 1; 3274 const out = []; 3275 for (let d = Math.ceil(min); d <= max; d++) if ((d - 1) % krok === 0) out.push({ value: d }); 3276 axis.ticks = out.length ? out : [{ value: Math.round(min) }, { value: Math.round(max) }]; 3277 } 3278 } 3279 } 3280 } 3281 }); 3282} 3283 3284function applySpotCustomRange() { 3285 const from = document.getElementById('spot-from').value; 3286 const to = document.getElementById('spot-to').value; 3287 if (!from || !to) return; 3288 document.querySelectorAll('[data-spot-range]').forEach(b => b.classList.remove('active')); 3289 __spotRange = 'custom'; 3290 drawSpotHourlyChart(from, to); 3291} 3292 3293function drawSpotHourlyChart(fromStr, toStr) { 3294 if (!window.spotData) return; 3295 const accent = getCssVar('--accent') || '#dc2626'; 3296 const muted = getCssVar('--text-muted') || '#71717a'; 3297 const border = getCssVar('--border') || '#e4e4e7'; 3298 const strong = getCssVar('--text-strong') || '#09090b'; 3299 const bgCard = getCssVar('--bg-card') || '#fff'; 3300 // VysokorozliÅ¡ovacà série s reálnými timestampy (hodinová + 15min kde jsou). 3301 const series = window.spotData.series || []; 3302 if (!series.length) return; 3303 const firstMs = series[0].x, lastMs = series[series.length - 1].x; 3304 // Äasové meze rozsahu 3305 let fromMs, toMs; 3306 if (fromStr && toStr) { 3307 fromMs = new Date(fromStr + 'T00:00:00').getTime(); 3308 toMs = new Date(toStr + 'T23:59:59').getTime(); 3309 } else { 3310 const DAY = 86400000; 3311 const spanDaysMap = { '7d': 7, '30d': 30, '90d': 90, '1y': 365, '3y': 3 * 365 }; 3312 toMs = lastMs; 3313 if (__spotRange === 'all') { 3314 fromMs = firstMs; 3315 } else if (__spotRange === 'ytd') { 3316 fromMs = Math.max(firstMs, new Date(new Date(lastMs).getFullYear(), 0, 1).getTime()); // 1. ledna roku poslednÃho bodu 3317 } else { 3318 fromMs = Math.max(firstMs, lastMs - (spanDaysMap[__spotRange] || 365) * DAY); 3319 } 3320 } 3321 // NaplÅ okýnka Od/Do skuteÄným rozsahem (vždy vyplnÄná, bÃlá; YTD â 1.1. â dneÅ¡ek apod.) 3322 const fEl = document.getElementById('spot-from'), tEl = document.getElementById('spot-to'); 3323 if (fEl) fEl.value = spotFmtInput(fromMs); 3324 if (tEl) tEl.value = spotFmtInput(toMs); 3325 // Body v rozsahu (série je seÅazená podle Äasu) 3326 const pts = series.filter(p => p.x >= fromMs && p.x <= toMs); 3327 // Statistiky z PLNÃHO rozsahu (ne z pÅÃpadnÄ podvzorkovaných bodů) 3328 let sum = 0, cnt = 0, mn = Infinity, mx = -Infinity; 3329 for (let i = 0; i < pts.length; i++) { 3330 const v = cvMs(pts[i].y, pts[i].x, 'spot5'); 3331 if (!isFinite(v)) continue; 3332 sum += v; cnt++; 3333 if (v < mn) mn = v; 3334 if (v > mx) mx = v; 3335 } 3336 const avg = cnt ? sum / cnt : 0; 3337 if (!cnt) { mn = 0; mx = 0; } 3338 // Plné rozliÅ¡enÃ; podvzorkuj jen u opravdu velkých rozsahů a i tak zachovej extrémy. 3339 const maxPoints = 15000; 3340 let data; 3341 if (pts.length <= maxPoints) { 3342 data = pts; 3343 } else { 3344 const stepN = Math.ceil(pts.length / maxPoints); 3345 data = []; 3346 for (let b = 0; b < pts.length; b += stepN) { 3347 const e = Math.min(b + stepN, pts.length);
3348 let minI = -1, maxI = -1, minV = Infinity, maxV = -Infinity; 3349 for (let i = b; i < e; i++) { 3350 const v = pts[i].y; 3351 if (!isFinite(v)) continue; 3352 if (v < minV) { minV = v; minI = i; } 3353 if (v > maxV) { maxV = v; maxI = i; } 3354 } 3355 if (minI < 0) continue; 3356 const lo = Math.min(minI, maxI), hi = Math.max(minI, maxI); 3357 data.push(pts[lo]); 3358 if (hi !== lo) data.push(pts[hi]); 3359 } 3360 } 3361 const spanDays = (toMs - fromMs) / 86400000; 3362 const stats = document.getElementById('spot-stats'); 3363 if (stats) stats.textContent = `PrůmÄr ${avg.toFixed(1)} ⢠Min ${mn.toFixed(1)} ⢠Max ${mx.toFixed(1)} ${ccyUnit('spot5')} (${cnt.toLocaleString('cs-CZ')} bodů${data.length < pts.length ? ', zobrazeno ' + data.length.toLocaleString('cs-CZ') : ''})`; 3364 if (spotHourlyChart) spotHourlyChart.destroy(); 3365 const ctx = document.getElementById('spot-hourly-chart').getContext('2d'); 3366 spotHourlyChart = new Chart(ctx, { 3367 type: 'line', 3368 data: { datasets: [{ label: 'Spot ' + ccyUnit('spot5'), data: data.map(p => ({ x: p.x, y: cvMs(p.y, p.x, 'spot5') })), borderColor: accent, backgroundColor: hexToRgba(accent, 0.15), fill: 'origin', tension: 0, pointRadius: 0, pointHoverRadius: 4, borderWidth: 0.8, spanGaps: false }] }, 3369 options: { 3370 responsive: true, maintainAspectRatio: false, 3371 animation: false, 3372 interaction: { mode: 'nearest', intersect: false }, 3373 plugins: { 3374 legend: { display: false }, 3375 tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8, 3376 callbacks: { 3377 title: items => items.length ? new Date(items[0].parsed.x).toLocaleString('cs-CZ') : '', 3378 label: c => c.parsed.y.toFixed(2) + ' ' + ccyUnit('spot5') 3379 } } 3380 }, 3381 scales: { 3382 y: { title: {display: true, text: ccyUnit('spot5'), color: muted}, grid: {color: border}, ticks: {color: muted} }, 3383 x: { type: 'linear', min: fromMs, max: toMs, ticks: {color: muted, maxTicksLimit: 12, callback: function(v) { 3384 const d = new Date(v); 3385 if (spanDays <= 3) return d.toLocaleDateString('cs-CZ', {day:'2-digit', month:'2-digit'}) + ' ' + d.getHours() + 'h'; 3386 if (spanDays <= 100) return d.toLocaleDateString('cs-CZ', {day:'2-digit', month:'short'}); 3387 return d.toLocaleDateString('cs-CZ', {month:'short', year:'2-digit'}); 3388 }}, grid: {display: false} } 3389 } 3390 } 3391 }); 3392} 3393 3394// ===== Multi-year hourly + DoW profile výpoÄet ze surových cen ===== 3395// spotData.prices je hodinová série od spotData.firstDate (2020-01-01 00:00). 3396// Pro daný rok spoÄÃtáme avg per hour-of-day (0-23) nebo per day-of-week (0-6, Po=0). 3397// Cache výsledků v window.spotData._cachedHourly / _cachedDow. 3398 3399function _computeSpotProfiles(year) { 3400 if (!window.spotData || !Array.isArray(spotData.prices) || !spotData.firstDate) return null; 3401 window.spotData._cachedHourly = window.spotData._cachedHourly || {}; 3402 window.spotData._cachedDow = window.spotData._cachedDow || {}; 3403 if (window.spotData._cachedHourly[year] && window.spotData._cachedDow[year]) return true; 3404 // first hour 3405 const firstD = new Date(spotData.firstDate.replace(' ', 'T') + ':00Z'); 3406 const firstMs = firstD.getTime(); 3407 const prices = spotData.prices; 3408 const hourSum = new Array(24).fill(0), hourCount = new Array(24).fill(0); 3409 const dowSum = new Array(7).fill(0), dowCount = new Array(7).fill(0); 3410 for (let i = 0; i < prices.length; i++) { 3411 const v = prices[i]; 3412 if (v === null || v === undefined || !Number.isFinite(v)) continue; 3413 const d = new Date(firstMs + i * 3600 * 1000); 3414 if (d.getUTCFullYear() !== year) continue; 3415 const h = d.getUTCHours(); 3416 hourSum[h] += v; hourCount[h]++; 3417 // ISO weekday: 0=Sun, 1=Mon,...,6=Sat. We want Po=0,...,Ne=6 3418 let dow = d.getUTCDay(); // 0=Sun 3419 dow = (dow === 0) ? 6 : (dow - 1); // 0=Mon,...,6=Sun 3420 dowSum[dow] += v; dowCount[dow]++; 3421 } 3422 const hourAvg = hourSum.map((s,i) => hourCount[i] > 0 ? s / hourCount[i] : null); 3423 const dowAvg = dowSum.map((s,i) => dowCount[i] > 0 ? s / dowCount[i] : null); 3424 window.spotData._cachedHourly[year] = hourAvg; 3425 window.spotData._cachedDow[year] = dowAvg; 3426 return true; 3427} 3428 3429function getSpotHourData(yearKey) { 3430 if (yearKey === '3yr') return window.spotData?.avgByHour3yr || null; 3431 // Hodinový profil bere pouze prebuilt agregáty (CET timestamps z původnÃho Excelu).
3432 // Roky bez prebuilt (2020-2022 a 2026) by z plochého spotData.prices vyÅ¡ly Å¡patnÄ 3433 // â mezery v sérii (1481 hodin) rozhodà mapovánà i mod 24 â CET hour. 3434 // Bude doplnÄno pÅes ENTSOE day-ahead API s explicitnÃmi timestamps. 3435 if (window.spotData?.avgByHourByYear?.[yearKey]) return window.spotData.avgByHourByYear[yearKey]; 3436 return null; 3437} 3438// Day-of-week profil z dailyByYear (per-day data se SPRÃVNÃMI timestampy). 3439// dailyByYear[year][i] = průmÄrná dennà cena pro i-tý den roku (i=0 = 1. ledna). 3440function _computeDowFromDaily(yearKey) { 3441 if (yearKey === '3yr') return window.spotData?.avgByDow3yr || null; 3442 const y = parseInt(yearKey, 10); 3443 if (!isFinite(y)) return null; 3444 const dailyArr = window.spotData?.dailyByYear?.[yearKey]; 3445 if (!dailyArr) return null; 3446 // Den-of-week 1. ledna daného roku (Mon=0,...,Sun=6) 3447 const jan1 = new Date(Date.UTC(y, 0, 1)); 3448 const jsDow = jan1.getUTCDay(); // 0=Sun 3449 const startDow = (jsDow === 0) ? 6 : (jsDow - 1); // 0=Mon 3450 const dowSum = new Array(7).fill(0), dowCnt = new Array(7).fill(0); 3451 dailyArr.forEach((v, i) => { 3452 if (v === null || v === undefined || !Number.isFinite(v)) return; 3453 const dow = (startDow + i) % 7; 3454 dowSum[dow] += v; 3455 dowCnt[dow]++; 3456 }); 3457 return dowSum.map((s, i) => dowCnt[i] > 0 ? s / dowCnt[i] : null); 3458} 3459 3460function getSpotDowData(yearKey) { 3461 if (yearKey === '3yr') return window.spotData?.avgByDow3yr || null; 3462 // NepoužÃvej prebuilt avgByDowByYear (nemusà mÃt vÅ¡echny roky) â vždy poÄÃtej z dailyByYear (správné timestampy) 3463 window.spotData = window.spotData || {}; 3464 window.spotData._cachedDowFromDaily = window.spotData._cachedDowFromDaily || {}; 3465 if (window.spotData._cachedDowFromDaily[yearKey] !== undefined) return window.spotData._cachedDowFromDaily[yearKey]; 3466 const result = _computeDowFromDaily(yearKey); 3467 window.spotData._cachedDowFromDaily[yearKey] = result; 3468 return result; 3469} 3470 3471// Multi-select state 3472window.__spotHourYears = new Set(['3yr']); 3473window.__spotDowYears = new Set(['3yr']); 3474 3475const YEAR_PALETTE = { 3476 '3yr': '#71717a', // grafit (průmÄr, ÄárkovanÄ) 3477 '2020': '#737373', // Å¡edá 3478 '2021': '#8b5cf6', // fialová 3479 '2022': '#3b82f6', // modrá 3480 '2023': '#06b6d4', // tyrkysová 3481 '2024': '#10b981', // zelená 3482 '2025': '#f59e0b', // oranžová 3483 '2026': '#dc2626' // Äervená 3484}; 3485 3486function toggleSpotHourYear(year) { 3487 const s = window.__spotHourYears; 3488 if (s.has(year)) s.delete(year); else s.add(year); 3489 if (s.size === 0) s.add('3yr'); // vždy aspoÅ 1 vybraný 3490 document.querySelectorAll('[data-spot-year]').forEach(b => b.classList.toggle('active', s.has(b.dataset.spotYear))); 3491 drawSpotHourChart(); 3492} 3493 3494function drawSpotHourChart() { 3495 if (!window.spotData) return; 3496 const accent = getCssVar('--accent') || '#dc2626'; 3497 const muted = getCssVar('--text-muted') || '#71717a'; 3498 const border = getCssVar('--border') || '#e4e4e7'; 3499 const strong = getCssVar('--text-strong') || '#09090b'; 3500 const bgCard = getCssVar('--bg-card') || '#fff'; 3501 const labels = Array.from({length: 24}, (_, i) => String(i).padStart(2,'0') + ':00'); 3502 const selected = Array.from(window.__spotHourYears); 3503 if (spotHourChart) spotHourChart.destroy(); 3504 const ctx = document.getElementById('spot-hour-chart').getContext('2d'); 3505 3506 if (selected.length === 1) { 3507 // Single year â bar chart se zelenýâÄervený gradient 3508 const _rate = fxYearKeyRate(selected[0]); 3509 const data = (getSpotHourData(selected[0]) || []).map(v => v == null ? null : cvRate(v, _rate, 'hour')); 3510 const valid = data.filter(v => v !== null); 3511 if (!valid.length) return; 3512 const mn = Math.min(...valid), mx = Math.max(...valid); 3513 const colors = data.map(v => { 3514 if (v === null) return 'transparent'; 3515 const t = mx === mn ? 0 : (v - mn) / (mx - mn); 3516 return interpolateColor(getCssVar('--delta-down') || '#047857', accent, t); 3517 }); 3518 spotHourChart = new Chart(ctx, { 3519 type: 'bar', 3520 data: { labels: labels, datasets: [{ label: labelForYear(selected[0]), data: data, backgroundColor: colors, borderRadius: 4 }] }, 3521 options: { 3522 responsive: true, maintainAspectRatio: false, 3523 plugins: { 3524 legend: { display: false }, 3525 tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8, 3526 callbacks: { label: ctx => ctx.parsed.y.toFixed(2).replace('.', ',') + ' ' + ccyUnit('hour') } } 3527 }, 3528 scales: { 3529 y: { title: {display: true, text: ccyUnit('hour'), color: muted}, grid: {color: border}, ticks: {color: muted} }, 3530 x: { title: {display: true, text: 'Hodina', color: muted}, grid: {display: false}, ticks: {color: muted, maxRotation: 0} } 3531 } 3532 } 3533 }); 3534 } else { 3535 // Multi-year â line chart, jedna Äára per rok 3536 const datasets = selected.map(yk => { 3537 const _rate = fxYearKeyRate(yk); 3538 const data = (getSpotHourData(yk) || []).map(v => v == null ? null : cvRate(v, _rate, 'hour')); 3539 const color = paletteColor(YEAR_PALETTE[yk] || '#71717a'); 3540 return { 3541 label: labelForYear(yk), 3542 data: data, 3543 borderColor: color, 3544 backgroundColor: color + '20', 3545 pointRadius: 0, 3546 pointHoverRadius: 5, 3547 borderWidth: yk === '3yr' ? 1.5 : 2, 3548 borderDash: yk === '3yr' ? [4, 3] : [], 3549 tension: 0.3, 3550 spanGaps: true 3551 }; 3552 }); 3553 spotHourChart = new Chart(ctx, { 3554 type: 'line', 3555 data: { labels: labels, datasets: datasets }, 3556 options: { 3557 responsive: true, maintainAspectRatio: false, 3558 interaction: { mode: 'index', intersect: false }, 3559 plugins: { 3560 legend: { display: true, position: 'top', labels: { color: muted, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12, boxHeight: 8 } }, 3561 tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8, 3562 callbacks: { label: ctx => ctx.dataset.label + ': ' + ctx.parsed.y.toFixed(2).replace('.', ',') + ' ' + ccyUnit('hour') } } 3563 }, 3564 scales: { 3565 y: { title: {display: true, text: ccyUnit('hour'), color: muted}, grid: {color: border}, ticks: {color: muted} }, 3566 x: { title: {display: true, text: 'Hodina', color: muted}, grid: {display: false}, ticks: {color: muted, maxRotation: 0} } 3567 } 3568 } 3569 }); 3570 } 3571} 3572 3573function toggleSpotDowYear(year) { 3574 const s = window.__spotDowYears; 3575 if (s.has(year)) s.delete(year); else s.add(year); 3576 if (s.size === 0) s.add('3yr'); 3577 document.querySelectorAll('[data-spot-dow-year]').forEach(b => b.classList.toggle('active', s.has(b.dataset.spotDowYear))); 3578 drawSpotDowChart(); 3579} 3580 3581function labelForYear(yk) { 3582 if (yk === '3yr') return '2023-2025 (průmÄr)'; 3583 if (yk === '2026') return '2026 (YTD)'; 3584 return yk; 3585} 3586 3587function drawSpotDowChart() { 3588 if (!window.spotData) return; 3589 const accent = getCssVar('--accent') || '#dc2626'; 3590 const muted = getCssVar('--text-muted') || '#71717a'; 3591 const border = getCssVar('--border') || '#e4e4e7'; 3592 const strong = getCssVar('--text-strong') || '#09090b'; 3593 const bgCard = getCssVar('--bg-card') || '#fff'; 3594 const selected = Array.from(window.__spotDowYears); 3595 if (spotDowChart) spotDowChart.destroy(); 3596 const ctx = document.getElementById('spot-dow-chart').getContext('2d'); 3597 3598 if (selected.length === 1) { 3599 const _rate = fxYearKeyRate(selected[0]); 3600 const data = (getSpotDowData(selected[0]) || []).map(v => v == null ? null : cvRate(v, _rate, 'dow')); 3601 const valid = data.filter(v => v !== null); 3602 if (!valid.length) return; 3603 const mn = Math.min(...valid), mx = Math.max(...valid); 3604 const colors = data.map(v => { 3605 if (v === null) return 'transparent'; 3606 const t = mx === mn ? 0 : (v - mn) / (mx - mn); 3607 return interpolateColor(getCssVar('--delta-down') || '#047857', accent, t); 3608 }); 3609 spotDowChart = new Chart(ctx, { 3610 type: 'bar', 3611 data: { labels: DAY_NAMES, datasets: [{ label: labelForYear(selected[0]), data: data, backgroundColor: colors, borderRadius: 6 }] }, 3612 options: { 3613 responsive: true, maintainAspectRatio: false, 3614 plugins: { 3615 legend: { display: false }, 3616 tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8, 3617 callbacks: { label: ctx => ctx.parsed.y.toFixed(2).replace('.', ',') + ' ' + ccyUnit('dow') } } 3618 }, 3619 scales: { 3620 y: { title: {display: true, text: ccyUnit('dow'), color: muted}, grid: {color: border}, ticks: {color: muted} }, 3621 x: { grid: {display: false}, ticks: {color: muted} } 3622 } 3623 } 3624 }); 3625 } else { 3626 // Multi-year: grouped bars (vedle sebe, 7 dnů à N roků) 3627 const datasets = selected.map(yk => { 3628 const _rate = fxYearKeyRate(yk); 3629 return { 3630 label: labelForYear(yk), 3631 data: (getSpotDowData(yk) || []).map(v => v == null ? null : cvRate(v, _rate, 'dow')), 3632 backgroundColor: paletteColor(YEAR_PALETTE[yk] || '#71717a'), 3633 borderRadius: 4, 3634 borderWidth: 0 3635 }; 3636 }); 3637 spotDowChart = new Chart(ctx, { 3638 type: 'bar', 3639 data: { labels: DAY_NAMES, datasets: datasets }, 3640 options: { 3641 responsive: true, maintainAspectRatio: false, 3642 plugins: { 3643 legend: { display: true, position: 'top', labels: { color: muted, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12, boxHeight: 8 } }, 3644 tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8, 3645 callbacks: { label: ctx => ctx.dataset.label + ': ' + ctx.parsed.y.toFixed(2).replace('.', ',') + ' ' + ccyUnit('dow') } } 3646 }, 3647 scales: { 3648 y: { title: {display: true, text: ccyUnit('dow'), color: muted}, grid: {color: border}, ticks: {color: muted} }, 3649 x: { grid: {display: false}, ticks: {color: muted} } 3650 } 3651 } 3652 }); 3653 } 3654} 3655 3656// ===== Sekce 13: Forward (PXE Cal) vs Spot (OTE dennà průmÄr) ===== 3657let fwdSpotChart = null; 3658 3659// RoÄnà průmÄry: forward (el_nextyear) vs spot (OTE dennà průmÄr) 3660function yearlyFwdSpotAverages() { 3661 const mean = a => (a && a.length) ? a.reduce((s, v) => s + v, 0) / a.length : null; 3662 const fwdByYear = {}, spotByYear = {}; 3663 const fwdPts = (typeof getSeriesPoints === 'function' ? getSeriesPoints('el_nextyear') : []);
3664 fwdPts.forEach(p => { 3665 const v = p.value; if (v === null || v === undefined || !isFinite(v)) return; 3666 const y = new Date(p.date).getUTCFullYear(); 3667 (fwdByYear[y] = fwdByYear[y] || []).push(cvDate(v, p.date, 'fwd')); 3668 }); 3669 fwdSpotDaily().forEach(p => { 3670 if (!isFinite(p.y)) return; 3671 const y = new Date(p.x).getUTCFullYear(); 3672 (spotByYear[y] = spotByYear[y] || []).push(cvMs(p.y, p.x, 'fwd')); 3673 }); 3674 // Spot 2018â2019: spot_hourly v DB zaÄÃná 2020 â roÄnà průmÄry doplnÄny z ENTSOE 3675 // day-ahead CZ (A44), pÅesný průmÄr 8 760 hodin/rok, extrakce 2026-06-10. Reálná data, ne odhad. 3676 const SPOT_AVG_STATIC = { 2018: 46.03, 2019: 40.21 }; // EUR/MWh 3677 Object.keys(SPOT_AVG_STATIC).forEach(y => { 3678 if (!spotByYear[y] || !spotByYear[y].length) { 3679 spotByYear[y] = [cvRate(SPOT_AVG_STATIC[y], fxYearAvg(parseInt(y, 10)), 'fwd')]; 3680 } 3681 }); 3682 const years = Array.from(new Set([...Object.keys(fwdByYear), ...Object.keys(spotByYear)].map(Number))) 3683 .filter(y => y >= 2008) // 2007: jen zlomek forwardových dat, zkresluje průmÄr â vynechat 3684 .sort((a, b) => a - b); 3685 return years.map(y => ({ year: y, fwd: mean(fwdByYear[y]), spot: mean(spotByYear[y]) })); 3686} 3687 3688// Spot jako dennà průmÄr (rekonstrukce z dailyByYear â reálné timestampy) 3689function fwdSpotDaily() { 3690 const out = []; 3691 const dby = window.spotData && window.spotData.dailyByYear; 3692 if (!dby) return out; 3693 Object.keys(dby).forEach(y => { 3694 const year = parseInt(y, 10); const arr = dby[y]; 3695 if (!Array.isArray(arr)) return; 3696 arr.forEach((v, i) => { 3697 if (v === null || v === undefined || !isFinite(v)) return; 3698 out.push({ x: Date.UTC(year, 0, 1) + i * 86400000, y: v }); 3699 }); 3700 }); 3701 out.sort((a, b) => a.x - b.x); 3702 return out; 3703} 3704 3705function drawForwardSpotChart() { 3706 const canvas = document.getElementById('fwd-spot-chart'); 3707 if (!canvas) return; 3708 const muted = getCssVar('--text-muted') || '#71717a'; 3709 const border = getCssVar('--border') || '#e4e4e7'; 3710 const strong = getCssVar('--text-strong') || '#09090b'; 3711 const bgCard = getCssVar('--bg-card') || '#fff'; 3712 const rows = yearlyFwdSpotAverages(); 3713 const thisYear = new Date().getFullYear(); 3714 const labels = rows.map(r => String(r.year) + (r.year === thisYear ? ' (YTD)' : '')); 3715 const fwdCol = (typeof getSeriesColor === 'function' ? getSeriesColor('el_nextyear') : '#6366f1'); 3716 const spotCol = '#06b6d4'; 3717 const r1 = v => (v === null || v === undefined) ? null : Math.round(v * 10) / 10; 3718 const ds = [ 3719 { label: 'Forward (à rok)', data: rows.map(r => r1(r.fwd)), backgroundColor: fwdCol, borderRadius: 4, maxBarThickness: 46, order: 2 }, 3720 { label: 'Spot (à rok)', data: rows.map(r => r1(r.spot)), backgroundColor: spotCol, borderRadius: 4, maxBarThickness: 46, order: 2 }, 3721 { label: 'Spot â Forward', type: 'line', data: rows.map(r => (r.fwd === null || r.spot === null) ? null : r1(r.spot - r.fwd)), 3722 borderColor: muted, borderDash: [5, 4], borderWidth: 1.5, pointRadius: 3, pointBackgroun
3722dColor: muted, 3723 spanGaps: false, tension: 0.2, order: 1 } 3724 ]; 3725 // Shrnutà pod graf: v kolika uzavÅených letech (kde existujà obÄ Åady) skonÄil spot pod forwardem 3726 const closed = rows.filter(r => r.year < thisYear && r.fwd !== null && r.spot !== null); 3727 const below = closed.filter(r => r.spot < r.fwd).length; 3728 const sumEl = document.getElementById('fwd-spot-summary'); 3729 if (sumEl && closed.length) { 3730 sumEl.textContent = ' Ve ' + below + ' z ' + closed.length + ' uzavÅených let s obÄma Åadami skonÄil spot POD forwardem (zajiÅ¡tÄnà bylo dražšà než spot).'; 3731 } 3732 if (fwdSpotChart) fwdSpotChart.destroy(); 3733 const ctx = canvas.getContext('2d'); 3734 fwdSpotChart = new Chart(ctx, { 3735 type: 'bar', 3736 data: { labels: labels, datasets: ds }, 3737 options: { 3738 responsive: true, maintainAspectRatio: false, animation: false, 3739 interaction: { mode: 'index', intersect: false }, 3740 plugins: { 3741 legend: { display: true, position: 'top', labels: { color: muted, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12, boxHeight: 8, padding: 12, font: {size: 12, weight: '500'} } }, 3742 tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8, 3743 callbacks: { 3744 label: c => c.dataset.label + ': ' + (c.parsed.y === null ? 'â' : c.parsed.y.toFixed(1).replace('.', ',') + ' ' + ccyUnit('fwd')), 3745 afterBody: items => { 3746 if (!items.length) return ''; 3747 const r = rows[items[0].dataIndex]; 3748 if (r.fwd === null || r.spot === null) return ''; 3749 const d = r.spot - r.fwd, sign = d >= 0 ? '+' : 'â'; 3750 const pct = Math.abs(d / r.fwd * 100).toFixed(1).replace('.', ','); 3751 return 'RozdÃl spot â forward: ' + sign + Math.abs(d).toFixed(1).replace('.', ',') + ' ' + ccyUnit('fwd') 3752 + ' (' + sign + pct + ' %)'; 3753 } 3754 } } 3755 }, 3756 scales: { 3757 y: { beginAtZero: true, title: {display: true, text: ccyUnit('fwd'), color: muted}, grid: {color: border}, ticks: {color: muted} }, 3758 x: { grid: {display: false}, ticks: {color: muted, font: {size: 12, weight: '500'}} } 3759 } 3760 } 3761 }); 3762} 3763 3764function toggleYear(year) { 3765 const btn = document.querySelector('[data-year="' + year + '"]'); 3766 btn.classList.toggle('active'); 3767 // Najdi dataset v Chart.js 3768 const idx = Object.keys(storageHistory).indexOf(year); 3769 if (storageHistoryChart && idx >= 0) { 3770 const meta = storageHistoryChart.getDatasetMeta(idx); 3771 meta.hidden = !btn.classList.contains('active'); 3772 storageHistoryChart.update(); 3773 } 3774} 3775 3776// ===== Client-side fetch z kurzy.cz JSONP (funguje i v bÄžném prohlÞeÄi) ===== 3777function fetchKurzyJSONP(chartId, cbName) { 3778 return new Promise((resolve, reject) => { 3779 const script = document.createElement('script'); 3780 const timeoutId = setTimeout(() => { cleanup(); reject(new Error('Timeout: ' + chartId)); }, 15000); 3781 function cleanup() { 3782 clearTimeout(timeoutId); 3783 try { delete window[cbName]; } catch (e) { window[cbName] = undefined; } 3784 if (script.parentNode) script.parentNode.removeChild(script); 3785 } 3786 window[cbName] = function(data) { cleanup(); resolve(data); }; 3787 script.onerror = function() { cleanup(); reject(new Error('JSONP script load failed: ' + chartId)); }; 3788 script.src = 'https://idb.kurzy.cz/' + chartId + '/history?callback=' + cbName; 3789 document.head.appendChild(script); 3790 }); 3791} 3792 3793function jsonpTsToTradingDate(tsMs) { 3794 // kurzy.cz timestamp = půlnoc CET (= 22:00 UTC pÅedchozÃho dne v DST). +12 h posune do dne obchodu. 3795 const d = new Date(tsMs + 12 * 3600 * 1000); 3796 const y = d.getUTCFullYear(); 3797 const m = String(d.getUTCMonth() + 1).padStart(2, '0'); 3798 const day = String(d.getUTCDate()).padStart(2, '0'); 3799 return y + '-' + m + '-' + day; 3800} 3801 3802function fmtDateCZShort(isoDate) { 3803 if (!isoDate) return 'â'; 3804 const parts = isoDate.split('-'); 3805 if (parts.length !== 3) return isoDate; 3806 return parseInt(parts[2], 10) + '. ' + parseInt(parts[1], 10) + '.'; 3807} 3808 3809function daysAgo(isoDate) { 3810 if (!isoDate) return null; 3811 const today = new Date(); 3812 today.setHours(0,0,0,0); 3813 const d = new Date(isoDate + 'T00:00:00'); 3814 return Math.round((today.getTime() - d.getTime()) / 86400000); 3815} 3816 3817function updateOverallFreshness() { 3818 // Najdi nejnovÄjšà datum napÅÃÄ hlavnÃmi sériemi (Cal-29 vynechané â je trvale stale) 3819 const liveSeries = ['el27','el28','gas27','gas28','gasTTF']; 3820 let newest = null;
3821 liveSeries.forEach(sid => { 3822 const s = window.HISTORICAL_DATA && HISTORICAL_DATA.series && HISTORICAL_DATA.series[sid]; 3823 if (s && s.points && s.points.length) { 3824 const d = s.points[s.points.length-1].date; 3825 if (!newest || d > newest) newest = d; 3826 } 3827 }); 3828 const el = document.getElementById('data-freshness-header'); 3829 if (!el) return; 3830 if (!newest) { el.innerHTML = ''; return; } 3831 const ago = daysAgo(newest); 3832 const months = ['ledna','února','bÅezna','dubna','kvÄtna','Äervna','Äervence','srpna','záÅÃ','ÅÃjna','listopadu','prosince']; 3833 const parts = newest.split('-'); 3834 const human = parseInt(parts[2], 10) + '. ' + months[parseInt(parts[1], 10) - 1]; 3835 let cls = 'fresh', icon = 'â'; 3836 if (ago > 1 && ago <= 3) { cls = 'aging'; icon = 'â '; } 3837 else if (ago > 3) { cls = 'stale'; icon = 'â '; } 3838 el.innerHTML = '<span class="freshness ' + cls + '">' + icon + ' Data ke dni ' + human + ' (poslednà trading close)</span>'; 3839 // Cal-29 sub-status 3840 const cal29 = HISTORICAL_DATA?.series?.el29?.points; 3841 if (cal29 && cal29.length) { 3842 const c29 = cal29[cal29.length-1].date; 3843 const c29ago = daysAgo(c29); 3844 if (c29ago > 3) { 3845 const c29Parts = c29.split('-'); 3846 const c29Human = parseInt(c29Parts[2], 10) + '. ' + months[parseInt(c29Parts[1], 10) - 1]; 3847 el.innerHTML += '<span class="freshness stale" title="Cal-29 nemá veÅejný zdroj â bez ENTSOE/EEX subscription se neaktualizuje">Cal-29: ' + c29Human + ' (' + c29ago + ' dnà zpÄt)</span>'; 3848 } 3849 } 3850} 3851 3852function updateFreshnessBadges() { 3853 try { 3854 if (!window.HISTORICAL_DATA || !HISTORICAL_DATA.series) { 3855 console.warn('updateFreshnessBadges: HISTORICAL_DATA not ready'); 3856 return; 3857 } 3858 const seriesLabelMap = { 3859 el27: 'Cal-27', el28: 'Cal-28', el29: 'Cal-29', 3860 gas27: 'Cal-27', gas28: 'Cal-28', gas29: 'Cal-29', gasTTF: 'TTF' 3861 }; 3862 const allKeys = Object.keys(HISTORICAL_DATA.series); 3863 console.log('updateFreshnessBadges: processing', allKeys.length, 'series'); 3864 allKeys.forEach(function(sid) { 3865 try { 3866 const series = HISTORICAL_DATA.series[sid]; 3867 if (!series || !series.points || !series.points.length) return; 3868 const last = series.points[series.points.length - 1]; 3869 if (!last || !last.date) return; 3870 const badges = document.querySelectorAll('[data-fresh="' + sid + '"]'); 3871 if (!badges.length) return; 3872 const ago = daysAgo(last.date); 3873 const seriesLabel = seriesLabelMap[sid] || sid; 3874 badges.forEach(function(badge) { 3875 try { 3876 const parent = badge.parentElement; 3877 const inMany = parent && parent.querySelectorAll('[data-fresh]').length > 1; 3878 let label = (inMany ? seriesLabel + ': ' : 'Aktualizováno ') + fmtDateCZShort(last.date); 3879 let cls = 'freshness'; 3880 if (ago !== null && ago !== undefined) { 3881 if (ago <= 1) cls += ' fresh'; 3882 else if (ago <= 3) { cls += ' aging'; if (!inMany) label += ' (' + ago + ' dny zpÄt)'; } 3883 else { cls += ' stale'; if (!inMany) label += ' (' + ago + ' dnà zpÄt)'; } 3884 } 3885 badge.className = cls; 3886 badge.textContent = label; 3887 const val = (last.value !== undefined && last.value !== null) ? last.value : '?'; 3888 badge.title = 'Série ' + sid + ': poslednà bod ' + last.date + ' = ' + val + (ago !== null ? ' (' + ago + ' dnà zpÄt)' : ''); 3889 } catch (eBadge) { 3890 console.error('updateFreshnessBadges badge error for', sid, eBadge); 3891 } 3892 }); 3893 } catch (eSeries) { 3894 console.error('updateFreshnessBadges series error for', sid, eSeries); 3895 } 3896 }); 3897 // ENTSOE freshness badges 3898 ['entsoeGen', 'entsoeFlows'].forEach(key => { 3899 const badge = document.querySelector('[data-fresh="' + key + '"]'); 3900 if (!badge || !window.entsoeData?.date) return; 3901 const ago = daysAgo(entsoeData.date); 3902 let cls = 'freshness'; 3903 if (ago <= 2) cls += ' fresh'; 3904 else if (ago <= 4) cls += ' aging'; 3905 else cls += ' stale'; 3906 badge.className = cls; 3907 badge.textContent = 'ENTSOE: ' + fmtDateCZShort(entsoeData.date); 3908 }); 3909 // Speciálnà badge pro ote15Data 3910 const oteBadge = document.querySelector('[data-fresh="ote15"]'); 3911 if (oteBadge && window.ote15Data) { 3912 const days = Object.keys(window.ote15Data).sort(); 3913 const lastDay = days[days.length-1]; 3914 const ago = daysAgo(lastDay); 3915 let label = 'Aktualizováno ' + fmtDateCZShort(lastDay);
3916 let cls = 'freshness'; 3917 if (ago === 0) cls += ' fresh'; 3918 else if (ago === 1) cls += ' fresh'; 3919 else if (ago <= 3) cls += ' aging'; 3920 else cls += ' stale'; 3921 oteBadge.className = cls; 3922 oteBadge.textContent = label; 3923 } 3924 } catch (eFatal) { 3925 console.error('updateFreshnessBadges FATAL:', eFatal); 3926 } 3927} 3928 3929async function refreshFromKurzy() { 3930 const banner = document.getElementById('status-banner'); 3931 banner.className = 'status-banner info'; 3932 banner.textContent = 'Stahuji nejnovÄjšà PXE Cal-27 ceny z kurzy.czâ¦'; 3933 const stamp = Date.now(); 3934 const [el27Raw, gas27Raw] = await Promise.all([ 3935 fetchKurzyJSONP('ch-22273-eur-1MWh', '__cbEl27_' + stamp), 3936 fetchKurzyJSONP('ch-22271-eur-1MWh', '__cbGas27_' + stamp) 3937 ]); 3938 function addPoints(seriesId, raw) { 3939 const series = HISTORICAL_DATA && HISTORICAL_DATA.series && HISTORICAL_DATA.series[seriesId]; 3940 if (!series || !Array.isArray(series.points)) return 0; 3941 const existing = new Set(series.points.map(p => p.date)); 3942 let added = 0; 3943 raw.forEach(function(row) { 3944 const ts = row[0], v = Number(row[1]); 3945 if (!Number.isFinite(ts) || !Number.isFinite(v)) return; 3946 const date = jsonpTsToTradingDate(ts); 3947 if (!existing.has(date)) { 3948 series.points.push({ date: date, value: v }); 3949 existing.add(date); 3950 added++; 3951 } 3952 }); 3953 series.points.sort(function(a, b) { return a.date.localeCompare(b.date); }); 3954 return added; 3955 } 3956 const elAdded = addPoints('el27', el27Raw); 3957 const gasAdded = addPoints('gas27', gas27Raw); 3958 updateLatestPriceTables(); 3959 updateFreshnessBadges(); 3960 updateOverallFreshness(); 3961 if (typeof drawPriceChart === 'function') { 3962 drawPriceChart('el', (priceChartState.el && priceChartState.el.range) || '1m'); 3963 drawPriceChart('gas', (priceChartState.gas && priceChartState.gas.range) || '1m'); 3964 } 3965 // Multi-Åádkový status banner: jeden Åádek per série 3966 banner.className = 'status-banner success'; 3967 const lines = ['<strong>â Aktualizace dokonÄena</strong>']; 3968 const seriesLabels = { 3969 el27: 'el. Cal-27', el28: 'el. Cal-28', el29: 'el. Cal-29', 3970 gas27: 'plyn Cal-27', gas28: 'plyn Cal-28', gas29: 'plyn Cal-29', gasTTF: 'TTF front-month' 3971 }; 3972 Object.entries(seriesLabels).forEach(([sid, label]) => { 3973 const series = HISTORICAL_DATA?.series?.[sid]; 3974 if (!series || !series.points || !series.points.length) return; 3975 const last = series.points[series.points.length - 1]; 3976 const ago = daysAgo(last.date); 3977 const added = sid === 'el27' ? elAdded : (sid === 'gas27' ? gasAdded : 0); 3978 let line = '⢠' + label + ': ' + last.value.toFixed(2).replace('.', ',') + ' EUR/MWh (' + fmtDateCZShort(last.date); 3979 if (ago !== null && ago > 1) line += ', ' + ago + ' dnà zpÄt'; 3980 line += ')'; 3981 if (added > 0) line += ' <strong>+' + added + ' nový</strong>'; 3982 lines.push(line); 3983 }); 3984 if (elAdded === 0 && gasAdded === 0) { 3985 lines.push('<em>Z kurzy.cz nic nového â data jsou aktuálnÃ. Cal-28/29 a TTF vyžadujà Cowork (CORS).</em>'); 3986 } 3987 banner.innerHTML = lines.join('<br>'); 3988} 3989 3990// ===== ENTSOE Transparency Platform data (Hodinová výroba + PÅeshraniÄnà bilance) ===== 3991window.entsoeHistory = {}; window.entsoeData = {date:null, generation_mw_hourly:{}, flows_mw_hourly:{}}; 3992window.entsoeData = Object.assign({date:"2026-06-01",timezone_note:"CEST aligned (UTC+2). Hour 0 = CEST 00:00 of date."}, window.entsoeHistory["2026-06-01"]); 3993 3994// Mapa typů zdrojů â {cs popisek, barva}. KlÃÄe pokrývajà OBÄ varianty názvů 3995// z DB: plné ENTSOE názvy ("Fossil Gas") i zkrácené ("Gas") â aby graf fungoval 3996// bez ohledu na to, který bÄh data zapsal. 3997const ENTSOE_GEN_TYPES = { 3998 'Nuclear': { cs: 'Jaderná', color: '#7c3aed' }, 3999 'Fossil Brown coal/Lignite': { cs: 'HnÄdé uhlÃ', color: '#78350f' }, 4000 'Lignite': { cs: 'HnÄdé uhlÃ', color: '#78350f' }, 4001 'Fossil Hard coal': { cs: 'Äerné uhlÃ', color: '#1c1917' }, 4002 'HardCoal': { cs: 'Äerné uhlÃ', color: '#1c1917' }, 4003 'Fossil Gas': { cs: 'Plyn', color: '#f59e0b' }, 4004 'Gas': { cs: 'Plyn', color: '#f59e0b' }, 4005 'Fossil Coal-derived gas': { cs: 'Plyn z uhlÃ', color: '#b45309' }, 4006 'CoalGas': { cs: 'Plyn z uhlÃ', color: '#b45309' }, 4007 'Fossil Oil': { cs: 'Olej', color: '#7f1d1d' }, 4008 'Oil': { cs: 'Olej', color: '#7f1d1d' }, 4009 'Solar': { cs: 'SolárnÃ', color: '#fbbf24' }, 4010 'Wind Onshore': { cs: 'VÃtr onshore', color: '#0ea5e9' }, 4011 'WindOnshore': { cs: 'VÃtr onshore', color: '#0ea5e9' }, 4012 'Wind Offshore': { cs: 'VÃtr offshore', color: '#0284c7' }, 4013 'WindOffshore': { cs: 'VÃtr offshore', color: '#0284c7' }, 4014 'Hydro Run-of-river': { cs: 'Vodnà (průtoÄná)', color: '#1d4ed8' }, 4015 'HydroROR': { cs: 'Vodnà (průtoÄná)', color: '#1d4ed8' }, 4016 'Hydro Water Reservoir': { cs: 'Vodnà (nádrž)', color: '#1e3a8a' }, 4017 'HydroReservoir': { cs: 'Vodnà (nádrž)', color: '#1e3a8a' }, 4018 'Hydro Pumped Storage': { cs: 'PÅeÄerpávacÃ', color: '#06b6d4' }, 4019 'PumpedStorage': { cs: 'PÅeÄerpávacÃ', color: '#06b6d4' }, 4020 'Biomass': { cs: 'Biomasa', color: '#16a34a' }, 4021 'Waste': { cs: 'Odpad', color: '#65a30d' }, 4022 'Other renewable': { cs: 'Ostatnà obnov.', color: '#10b981' }, 4023 'OtherRenewable': { cs: 'Ostatnà obnov.', color: '#10b981' }, 4024 'Geothermal': { cs: 'GeotermálnÃ', color: '#ea580c' }, 4025 'Other': { cs: 'OstatnÃ', color: '#71717a' } 4026}; 4027 4028let entsoeGenChart = null, entsoeFlowsChart = null; 4029window.__entsoeCurrentDate = null; 4030 4031function getEntsoeAvailableDates() { 4032 return Object.keys(window.entsoeHistory || {}).sort(); 4033} 4034 4035function setEntsoeDate(date) { 4036 const avail = getEntsoeAvailableDates(); 4037 if (!avail.length) return; 4038 let target; 4039 if (date === 'latest') target = avail[avail.length - 1]; 4040 else target = date; 4041 // Najdi nejbližšà dostupný (pokud zadané datum nenà v historii) 4042 const note = document.getElementById('entsoe-day-note'); 4043 if (window.entsoeHistory[target]) { 4044 window.__entsoeCurrentDate = target; 4045 window.entsoeData = Object.assign({date: target, timezone_note: 'CEST aligned (UTC+2). Hour 0 = CEST 00:00 of date.'}, window.entsoeHistory[target]); 4046 if (note) note.textContent = ''; 4047 } else { 4048 if (note) note.textContent = 'Data pro ' + target + ' nejsou k dispozici. Dostupné: ' + avail.join(', '); 4049 return; 4050 } 4051 // Update picker UI 4052 const picker = document.getElementById('entsoe-gen-picker'); 4053 if (picker) { 4054 picker.value = target; 4055 if (avail.length) { picker.min = avail[0]; picker.max = avail[avail.length-1]; } 4056 } 4057 drawEntsoeGenChart(); 4058 drawEntsoeFlowsChart(); 4059} 4060 4061function shiftEntsoeDate(delta) { 4062 const avail = getEntsoeAvailableDates(); 4063 if (!avail.length) return; 4064 const cur = window.__entsoeCurrentDate || avail[avail.length - 1]; 4065 const idx = avail.indexOf(cur); 4066 const newIdx = Math.max(0, Math.min(avail.length - 1, idx + delta)); 4067 setEntsoeDate(avail[newIdx]); 4068} 4069 4070function drawEntsoeGenChart() { 4071 if (!window.entsoeData?.generation_mw_hourly) return; 4072 const muted = getCssVar('--text-muted') || '#71717a'; 4073 const border = getCssVar('--border') || '#e4e4e7'; 4074 const strong = getCssVar('--text-strong') || '#09090b'; 4075 const bgCard = getCssVar('--bg-card') || '#fff'; 4076 const gen = entsoeData.generation_mw_hourly; 4077 const labels = Array.from({length: 24}, (_, i) => String(i).padStart(2,'0') + ':00'); 4078 // Sort series by total magnitude desc for nicer stacking 4079 const sortedKeys = Object.keys(gen).sort((a, b) => { 4080 const sa = gen[a].reduce((s,v)=>s+Math.abs(v),0); 4081 const sb = gen[b].reduce((s,v)=>s+Math.abs(v),0); 4082 return sb - sa; 4083 }); 4084 const datasets = sortedKeys.map(k => { 4085 const t = ENTSOE_GEN_TYPES[k] || { cs: k, color: '#a3a3a3' }; 4086 return { 4087 label: t.cs, 4088 data: gen[k], 4089 backgroundColor: paletteColor(t.color) + 'cc', 4090 borderColor: paletteColor(t.color), 4091 borderWidth: 0.5, 4092 fill: true, 4093 pointRadius: 0, 4094 tension: 0.2 4095 }; 4096 }); 4097 if (entsoeGenChart) entsoeGenChart.destroy(); 4098 const ctx = document.getElementById('entsoe-gen-chart').getContext('2d'); 4099 entsoeGenChart = new Chart(ctx, { 4100 type: 'line', 4101 data: { labels: labels, datasets: datasets }, 4102 options: { 4103 responsive: true, maintainAspectRatio: false, 4104 interaction: { mode: 'index', intersect: false }, 4105 plugins: { 4106 legend: { display: true, position: 'top', labels: { color: muted, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12, boxHeight: 8, padding: 8, font: {size: 11} } }, 4107 tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8, 4108 callbacks: { label: ctx => ctx.dataset.label + ': ' + Math.round(ctx.parsed.y).toLocaleStr
4108ing('cs-CZ') + ' MW' } } 4109 }, 4110 scales: { 4111 y: { stacked: true, beginAtZero: true, title: {display: true, text: 'MW', color: muted}, grid: {color: border}, ticks: {color: muted, callback: v => Math.round(v).toLocaleString('cs-CZ')} }, 4112 x: { title: {display: true, text: 'Hodina (CET)', color: muted}, grid: {display: false}, ticks: {color: muted, maxRotation: 0} } 4113 } 4114 } 4115 }); 4116 const dateEl = document.getElementById('entsoe-gen-date'); 4117 if (dateEl && entsoeData.date) { 4118 const p = entsoeData.date.split('-'); 4119 dateEl.textContent = parseInt(p[2], 10) + '. ' + parseInt(p[1], 10) + '. ' + p[0]; 4120 } 4121} 4122 4123// ===== Výroba jednotlivých elektráren ÄR (ENTSOE A73) ===== 4124window.entsoeUnitHistory = window.entsoeUnitHistory || {}; 4125window.entsoeUnitData = { date: null, units: {} }; 4126let entsoeUnitChart = null; 4127window.__entsoeUnitDate = null; 4128 4129const ENTSOE_PLANT_PALETTE = { 4130 'TemelÃn': '#7c3aed', // jádro â fialová 4131 'Dukovany': '#a78bfa', // jádro â svÄtle fialová 4132 'PoÄerady': '#78350f', // hnÄdé uhlà 4133 'PrunéÅov II': '#92400e', 4134 'TuÅ¡imice II': '#b45309', 4135 'Chvaletice': '#a16207', 4136 'Ledvice': '#854d0e', 4137 'Tisová': '#a8a29e', 4138 'MÄlnÃk': '#713f12', 4139 'VÅesová': '#f59e0b', // plyn â amber 4140 'Kladno': '#dc2626', // uhlÃ/plyn â Äervená pro odliÅ¡enà 4141 'DaleÅ¡ice (PVE)': '#06b6d4', // pÅeÄerpávacà â cyan 4142 'Dlouhé StránÄ (PVE)':'#0891b2' 4143}; 4144const PLANT_FALLBACK = ['#64748b','#0ea5e9','#16a34a','#db2777','#ca8a04','#4f46e5']; 4145 4146window.__unitPeriod = window.__unitPeriod || 1; // poÄet dnà v oknÄ (1 = jeden den) 4147window.__unitGroup = window.__unitGroup || 'plant'; // 'plant' | 'fuel' 4148 4149// Elektrárna â palivo (pro seskupenÃ) a barvy palivových skupin 4150const UNIT_FUEL = { 4151 'TemelÃn':'Jádro', 'Dukovany':'Jádro', 4152 'PoÄerady':'HnÄdé uhlÃ', 'PrunéÅov II':'HnÄdé uhlÃ', 'TuÅ¡imice II':'HnÄdé uhlÃ', 4153 'Chvaletice':'HnÄdé uhlÃ', 'Ledvice':'HnÄdé uhlÃ', 'Tisová':'HnÄdé uhlÃ', 'MÄlnÃk':'HnÄdé uhlÃ', 4154 'VÅesová':'Zemnà plyn', 'Kladno':'OstatnÃ', 4155 'DaleÅ¡ice (PVE)':'PÅeÄerpávacÃ', 'Dlouhé StránÄ (PVE)':'PÅeÄerpávacÃ' 4156}; 4157const FUEL_COLOR = { 'Jádro':'#7c3aed', 'HnÄdé uhlÃ':'#78350f', 'Zemnà plyn':'#f59e0b', 4158 'PÅeÄerpávacÃ':'#06b6d4', 'OstatnÃ':'#71717a' }; 4159 4160function getEntsoeUnitDates() { return Object.keys(window.entsoeUnitHistory || {}).sort(); } 4161 4162function setEntsoeUnitDate(date) { 4163 const avail = getEntsoeUnitDates(); 4164 if (!avail.length) return; 4165 const target = (date === 'latest') ? avail[avail.length - 1] : date; 4166 const note = document.getElementById('entsoe-unit-note'); 4167 if (window.entsoeUnitHistory[target]) { 4168 window.__entsoeUnitDate = target; 4169 if (note) note.textContent = ''; 4170 } else { 4171 if (note) note.textContent = 'Data pro ' + target + ' nejsou k dispozici.'; 4172 return; 4173 } 4174 const picker = document.getElementById('entsoe-unit-picker'); 4175 if (picker) { picker.value = target; picker.min = avail[0]; picker.max = avail[avail.length - 1]; } 4176 drawEntsoeUnitChart(); 4177} 4178function shiftEntsoeUnitDate(delta) { 4179 const avail = getEntsoeUnitDates(); 4180 if (!avail.length) return; 4181 const cur = window.__entsoeUnitDate || avail[avail.length - 1]; 4182 const idx = avail.indexOf(cur); 4183 setEntsoeUnitDate(avail[Math.max(0, Math.min(avail.length - 1, idx + delta))]); 4184} 4185function setUnitPeriod(p) { 4186 window.__unitPeriod = parseInt(p, 10); 4187 document.querySelectorAll('[data-unit-period]').forEach(b => b.classList.toggle('active', b.dataset.unitPeriod === String(p))); 4188 drawEntsoeUnitChart(); 4189} 4190function setUnitGroup(g) { 4191 window.__unitGroup = g; 4192 document.querySelectorAll('[data-unit-group]').forEach(b => b.classList.toggle('active', b.dataset.unitGroup === g)); 4193 drawEntsoeUnitChart(); 4194} 4195 4196// SpoÄÃtej profil {klÃÄ: [24]} jako PRÅ®MÄR za poslednÃch N dnà konÄÃcÃch zvoleným dnem. 4197function unitProfile() { 4198 const avail = getEntsoeUnitDates(); 4199 if (!avail.length) return { data: {}, days: [] }; 4200 const end = window.__entsoeUnitDate || avail[avail.length - 1]; 4201 const endIdx = Math.max(0, avail.indexOf(end));
4202 const N = window.__unitPeriod || 1; 4203 const days = avail.slice(Math.max(0, endIdx - N + 1), endIdx + 1); 4204 const acc = {}; 4205 days.forEach(d => { 4206 const day = window.entsoeUnitHistory[d] || {}; 4207 Object.keys(day).forEach(pl => { 4208 const arr = acc[pl] || (acc[pl] = new Array(24).fill(0)); 4209 for (let h = 0; h < 24; h++) arr[h] += (day[pl][h] || 0); 4210 }); 4211 }); 4212 const cnt = days.length || 1; 4213 const out = {}; 4214 if (window.__unitGroup === 'fuel') { 4215 Object.keys(acc).forEach(pl => { 4216 const f = UNIT_FUEL[pl] || 'OstatnÃ'; 4217 const arr = out[f] || (out[f] = new Array(24).fill(0)); 4218 for (let h = 0; h < 24; h++) arr[h] += acc[pl][h] / cnt; 4219 }); 4220 } else { 4221 Object.keys(acc).forEach(pl => { out[pl] = acc[pl].map(v => v / cnt); }); 4222 } 4223 return { data: out, days: days }; 4224} 4225 4226function drawEntsoeUnitChart() { 4227 const muted = getCssVar('--text-muted') || '#71717a'; 4228 const border = getCssVar('--border') || '#e4e4e7'; 4229 const strong = getCssVar('--text-strong') || '#09090b'; 4230 const bgCard = getCssVar('--bg-card') || '#fff'; 4231 const { data: u, days } = unitProfile(); 4232 const labels = Array.from({length: 24}, (_, i) => String(i).padStart(2,'0') + ':00'); 4233 const sortedKeys = Object.keys(u).sort((a, b) => 4234 u[b].reduce((s,v)=>s+Math.abs(v),0) - u[a].reduce((s,v)=>s+Math.abs(v),0)); 4235 let fb = 0; 4236 const datasets = sortedKeys.map(k => { 4237 const color = (window.__unitGroup === 'fuel') 4238 ? (FUEL_COLOR[k] || '#a3a3a3') 4239 : (ENTSOE_PLANT_PALETTE[k] || PLANT_FALLBACK[(fb++) % PLANT_FALLBACK.length]); 4240 return { label: k, data: u[k], backgroundColor: color + 'cc', borderColor: color, 4241 borderWidth: 0.5, fill: true, pointRadius: 0, tension: 0.2 }; 4242 }); 4243 if (entsoeUnitChart) entsoeUnitChart.destroy(); 4244 const ctx = document.getElementById('entsoe-unit-chart').getContext('2d'); 4245 entsoeUnitChart = new Chart(ctx, { 4246 type: 'line', 4247 data: { labels: labels, datasets: datasets }, 4248 options: { 4249 responsive: true, maintainAspectRatio: false, 4250 interaction: { mode: 'index', intersect: false }, 4251 plugins: { 4252 legend: { display: true, position: 'top', labels: { color: muted, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12, boxHeight: 8, padding: 8, font: {size: 11} } }, 4253 tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8, 4254 callbacks: { label: ctx => ctx.dataset.label + ': ' + Math.round(ctx.parsed.y).toLocaleString('cs-CZ') + ' MW' } } 4255 }, 4256 scales: { 4257 y: { stacked: true, beginAtZero: true, title: {display: true, text: 'MW', color: muted}, grid: {color: border}, ticks: {color: muted, callback: v => Math.round(v).toLocaleString('cs-CZ')} }, 4258 x: { title: {display: true, text: 'Hodina (CET)', color: muted}, grid: {display: false}, ticks: {color: muted, maxRotation: 0} } 4259 } 4260 } 4261 }); 4262 const dateEl = document.getElementById('entsoe-unit-date'); 4263 if (dateEl && days.length) { 4264 const fmt = s => { const p = s.split('-'); return parseInt(p[2],10) + '. ' + parseInt(p[1],10) + '.'; }; 4265 dateEl.textContent = (days.length === 1) 4266 ? fmt(days[0]) + ' ' + days[0].slice(0,4) 4267 : 'à ' + days.length + ' dnà (' + fmt(days[0]) + 'â' + fmt(days[days.length-1]) + ' ' + days[days.length-1].slice(0,4) + ')'; 4268 } 4269} 4270 4271// ===== Teplárenstvà â statická kurátorská data (roÄnÃ, z veÅejných zdrojů) ===== 4272// Pro teplo NEEXISTUJE real-time/hodinový open feed (na rozdÃl od elektÅiny/ENTSOE). 4273// ERà RoÄnà zprávy o provozu teplárenských soustav ÄR (XLSX, list 4.1 â výroba tepla 4274// brutto podle paliv, celá ÄR, TJ). PodÃly spoÄteny z oficiálnÃch roÄnÃch souÄtů, 4275// extrakce 2026-06-10 z eru.gov.cz (eruteplo2021â2024.xlsx). Žádné odhady. 4276const HEAT_FUEL_COLORS = { hnede: '#78350f', plyn: '#f59e0b', biomasa: '#16a34a', cerne: '#1c1917', ostatni: '#71717a' }; 4277const HEAT_FUEL_BY_YEAR = { 4278 '2021': { total_tj: 161657, hnede: 37.5, plyn: 21.3, biomasa: 14.7, cerne: 9.0, ostatni: 17.5 }, 4279 '2022': { total_tj: 151093, hnede: 39.1, plyn: 19.6, biomasa: 15.0, cerne: 8.3, ostatni: 18.0 }, 4280 '2023': { total_tj: 140923, hnede: 38.5, plyn: 19.5, biomasa: 15.5, cerne: 7.7, ostatni: 18.8 }, 4281 '2024': { total_tj: 132837, hnede: 35.2, plyn: 20.9, biomasa: 19.3, cerne: 5.9, ostatni: 18.7 } 4282}; 4283window.__heatFuelYear = '2024'; 4284function setHeatFuelYear(y) { 4285 window.__heatFuelYear = y; 4286 document.querySelectorAll('[data-heat-fuel-year]').forEach(b => b.classList.toggle('active', b.dataset.heatFuelYear === y)); 4287 drawHeatFuelChart(); 4288} 4289// ERà RoÄnà zprávy o provozu teplárenských soustav ÄR (XLSX, list 5.2 â dodávky tepla 4290// v krajÃch, TJ/rok). Oficiálnà roÄnà souÄty, extrakce 2026-06-10 z eru.gov.cz. 4291// (Původnà per-operátor data nahrazena: konzistentnà Åada po letech pro operátory 4292// veÅejnÄ neexistuje â mix kalendáÅnÃch/hospodáÅských roků a výroby/prodeje.) 4293const HEAT_KRAJE_BY_YEAR = { 4294 '2021': { 'Praha': 4513, 'JihoÄeský': 5113, 'Jihomoravský': 5788, 'Karlovarský': 3504, 'VysoÄina': 1747, 'Královéhradecký': 3086, 'Liberecký': 2233, 'Moravskoslezský': 16076, 'Olomoucký': 3554, 'Pardubický': 4406, 'PlzeÅský': 4366, 'StÅedoÄeský': 21200, 'Ãstecký': 12750, 'ZlÃnský': 4092 }, 4295 '2022': { 'Praha': 3793, 'JihoÄeský': 4658, 'Jihomoravský': 5176, 'Karlovarský': 3258, 'VysoÄina': 1539, 'Královéhradecký': 2837, 'Liberecký': 1952, 'Moravskoslezský': 14000, 'Olomoucký': 3153, 'Pardubický': 3903, 'PlzeÅský': 3908, 'StÅedoÄeský': 18710, 'Ãstecký': 11516, 'ZlÃnský': 3668 }, 4296 '2023': { 'Praha': 3382, 'JihoÄeský': 4235, 'Jihomoravský': 4828, 'Karlovarský': 3265, 'VysoÄina': 1466, 'Královéhradecký': 2685, 'Liberecký': 1869, 'Moravskoslezský': 12536, 'Olomoucký': 3010, 'Pardubický': 3680, 'PlzeÅský': 3727, 'StÅedoÄeský': 16946, 'Ãstecký': 10840, 'ZlÃnský': 3331 }, 4297 '2024': { 'Praha': 3521, 'JihoÄeský': 3961, 'Jihomoravský': 4746, 'Karlovarský': 3164, 'VysoÄina': 1487, 'Královéhradecký': 2639, 'Liberecký': 1800, 'Moravskoslezský': 10002, 'Olomoucký': 2949, 'Pardubický': 3552, 'PlzeÅský': 3713, 'StÅedoÄeský': 17103, 'Ãstecký': 11167, 'ZlÃnský': 3199 } 4298}; 4299window.__heatPlantsYear = '2024'; 4300function setHeatPlantsYear(y) { 4301 window.__heatPlantsYear = y; 4302 document.querySelectorAll('[data-heat-plants-year]').forEach(b => b.classList.toggle('active', b.dataset.heatPlantsYear === y)); 4303 drawHeatPlantsChart(); 4304} 4305let heatFuelChart = null, heatPlantsChart = null; 4306 4307// ===== Sekce 18: Energeticke vyuziti odpadu a TAP (MPO 1989-2024) ===== 4308// Zdroj: MPO "Statistika energetickeho vyuzivani odpadu a alternativnich paliv
43081989-2024" 4309// (rijen 2025), str. 20 (sektory) a str. 15 (cementarny). Extrahovano z PDF 2026-06-11, 4310// soucty krizove overeny. Aktualizace 1x rocne po vydani MPO (~rijen). 4311const TAP_YEARS = [1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024]; 4312const TAP_S_TKO = [44685, 65550, 57710, 82200, 101800, 156425, 163115, 171000, 174127, 244535, 284646, 333572, 382025, 403178, 407820, 409288, 388303, 391930, 391620, 376381, 360399, 469003, 613082, 634280, 637627, 642806, 631908, 666018, 702831, 668258, 684918, 715968, 720391, 701379, 815666, 830348]; 4313const TAP_S_PRO = [12050, 14100, 16880, 19810, 27050, 38355, 43065, 43290, 45371, 45588, 46572, 38396, 52461, 56144, 65835, 64553, 61347, 59038, 76086, 78025, 68029, 72341, 78547, 80130, 78551, 82333, 89130, 81313, 89150, 94283, 91420, 90373, 86795, 80541, 79527, 65777]; 4314const TAP_S_CEM = [0, 0, 0, 0, 0, 0, 0, 0, 20594, 37824, 43112, 66497, 90486, 77980, 90534, 151368, 187504, 174708, 194904, 219764, 245543, 252708, 268552, 296476, 274142, 336658, 372506, 396769, 395187, 439781, 465513, 470904, 486565, 469657, 380160, 390928]; 4315const TAP_S_ENE = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9745, 2002, 1228, 2420, 3189, 3875, 1807, 1978, 6567, 4765, 5462, 4429, 4467, 2952, 2266, 2629, 30537, 71732]; 4316const TAP_S_TOTAL = [56735, 79650, 74590, 102010, 128850, 194780, 206180, 214290, 240092, 327947, 374330, 438465, 524972, 537302, 564189, 625209, 637154, 625676, 672355, 676172, 675199, 796472, 963370, 1014761, 992127, 1063775, 1100111, 1148865, 1192480, 1207170, 1248044, 1281386, 1297594, 1255587, 1306598, 1359120]; 4317const TAP_S_GJ = [688550, 938700, 914020, 1204075, 1531864, 2266111, 2386593, 2456393, 3085501, 4150493, 4625892, 5561421, 6865857, 6727959, 7318852, 8832778, 9545248, 9039837, 10308320, 10376719, 10475555, 11033230, 12552627, 12981617, 12617584, 14038061, 14950786, 16330153, 16650802, 18191055, 19039972, 19440981, 19248854, 18717926, 17959064, 19105142]; 4318const TAP_CEM_YEARS = [1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024]; 4319const TAP_C_TAP = [70, 75, 3489, 7191, 9856, 22211, 31850, 51537, 68704, 66111, 104510, 107131, 146142, 165010, 180307, 203198, 176260, 232674, 261021, 279822, 290983, 317496, 340013, 350661, 367935, 345505, 281354, 266775]; 4320const TAP_C_ADIT = [0, 0, 1069, 9829, 15029, 13579, 14521, 18562, 12923, 19780, 18711, 20620, 18844, 19680, 16406, 27447, 28655, 32479, 29044, 14227, 18136, 2745, 4122, 3509, 1773, 364, 1276, 1260]; 4321const TAP_C_PNEU = [13709, 15316, 18242, 22517, 27640, 27101, 27918, 36796, 42893, 37319, 38127, 44411, 44902, 45537, 50756, 47252, 49064, 34134, 46892, 66977, 57702, 62818, 71203, 57702, 58560, 77083, 55783, 68039]; 4322const TAP_C_KAP = [6815, 22433, 20312, 26939, 36387, 10979, 11101, 15458, 21703, 18842, 12512, 14870, 8128, 6130, 5837, 5247, 2678, 6713, 5791, 10995, 11824, 10474, 10422, 10918, 7713, 7016, 3227, 6943]; 4323const TAP_C_OZE = [0, 0, 0, 21, 1574, 4110, 5144, 29015, 41283, 32657, 21045, 32732, 27528, 16351, 15246, 13332, 17485, 20273, 19422, 18196, 11329, 36780, 23706, 24239, 28656, 20907, 21504, 30233]; 4324const TAP_C_ODPAD = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10385, 10336, 6552, 5213, 9468, 16048, 23875, 21928, 18782, 17016, 17677]; 4325const TAP_C_TOTAL = [20594, 37824, 43112, 66497, 90486, 77980, 90534, 151368, 187504, 174708, 194904, 219764, 245543, 252708, 268552, 296476, 274142, 336658, 372506, 396769, 395187, 439781, 465513, 470904, 486565, 469657, 380160, 390928]; 4326const TAP_C_GJ = [561618, 1036236, 1148205, 1721110, 2345216, 1901382, 2201627, 3535648, 4445136, 4199668, 4556955, 5025604, 5597459, 5531634, 5662907, 5885365, 5575666, 6975079, 7781882, 8938830, 8709818, 10489698, 10988700, 11070601, 10908448, 10599523, 8456319, 8986321]; 4327let tapChart = null; 4328window.__tapView = 'sectors'; 4329function setTapView(v) { 4330 window.__tapView = v; 4331 document.querySelectorAll('[data-tap-view]').forEach(b => b.classList.toggle('active', b.dataset.tapView === v)); 4332 const ns = document.getElementById('tap-note-sectors'), nc = document.getElementById('tap-note-cement'); 4333 if (ns) ns.style.display = v === 'sectors' ? '' : 'none'; 4334 if (nc) nc.style.display = v === 'cement' ? '' : 'none'; 4335 drawTapChart(); 4336} 4337function drawTapChart() { 4338 const el = document.getElementById('tap-chart'); if (!el) return; 4339 const muted = getCssVar('--text-muted') || '#71717a'; 4340 const border = getCssVar('--border') || '#e4e4e7'; 4341 const strong = getCssVar('--text-strong') || '#09090b'; 4342 const bgCard = getCssVar('--bg-card') || '#fff'; 4343 let labels, datasets, y1title; 4344 if (window.__tapView === 'cement') { 4345 labels = TAP_CEM_YEARS.map(String); 4346 y1title = 'tuny / rok'; 4347 const mk = (lbl, data, color) => ({ label: lbl, data: data, backgroundColor: paletteColor(color), stack: 's', borderRadius: 2, maxBarThickness: 26 }); 4348 datasets = [ 4349 mk('Äistá TAP', TAP_C_TAP, '#3b82f6'), 4350 mk('Pneumatiky', TAP_C_PNEU, '#1c1917'), 4351 mk('Kapalná alt. paliva', TAP_C_KAP, '#f59e0b'), 4352 mk('OZE / biomasa', TAP_C_OZE, '#16a34a'), 4353 mk('Odpady pÅÃmo', TAP_C_ODPAD, '#71717a'), 4354 mk('Tuhá aditivnÃ', TAP_C_ADIT, '#a78bfa') 4355 ]; 4356 } else { 4357 labels = TAP_YEARS.map(String); 4358 y1title = 'tuny / rok'; 4359 const mk = (lbl, data, color) => ({ label: lbl, data: data, backgroundColor: paletteColor(color), stack: 's', borderRadius: 2, maxBarThickness: 26 }); 4360 datasets = [ 4361 mk('Spalovny TKO (ZEVO)', TAP_S_TKO, '#dc2626'), 4362 mk('Cementárny a vápenky', TAP_S_CEM, '#78350f'), 4363 mk('Prům. a nemocniÄnà spalovny', TAP_S_PRO, '#f59e0b'), 4364 mk('Energetika a teplárenstvÃ', TAP_S_ENE, '#10b981'), 4365 { label: 'Energie v palivu (TJ)', type: 'line', yAxisID: 'y2', 4366 data: TAP_S_GJ.map(v => Math.round(v / 1000)), 4367 borderColor: muted, borderDash: [5, 4], borderWidth: 1.5, pointRadius: 0, pointHoverRadius: 3, tension: 0.25 } 4368 ]; 4369 } 4370 if (tapChart) tapChart.destroy(); 4371 tapChart = new Chart(el.getContext('2d'), { 4372 type: 'bar', 4373 data: { labels: labels, datasets: datasets }, 4374 options: { 4375 responsive: true, maintainAspectRatio: false, animation: false, 4376 interaction: { mode: 'index', intersect: false }, 4377 plugins: { 4378 legend: { display: true, position: 'top', labels: { color: muted, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12, boxHeight: 8, padding: 10, font: {size: 11, weight: '500'} } }, 4379 tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8, 4380 callbacks: { 4381 label: c =>
4381 c.dataset.label + ': ' + c.parsed.y.toLocaleString('cs-CZ') + (c.dataset.yAxisID === 'y2' ? ' TJ' : ' t'), 4382 footer: items => { 4383 const i = items[0].dataIndex; 4384 if (window.__tapView === 'cement') { 4385 return 'Celkem: ' + TAP_C_TOTAL[i].toLocaleString('cs-CZ') + ' t | ' + Math.round(TAP_C_GJ[i] / 1000).toLocaleString('cs-CZ') + ' TJ'; 4386 } 4387 return 'Celkem (dle MPO): ' + TAP_S_TOTAL[i].toLocaleString('cs-CZ') + ' t'; 4388 } 4389 } } 4390 }, 4391 scales: { 4392 x: { stacked: true, grid: {display: false}, ticks: {color: muted, font: {size: 10}, maxRotation: 60, autoSkip: true} }, 4393 y: { stacked: true, beginAtZero: true, title: {display: true, text: y1title, color: muted}, grid: {color: border}, ticks: {color: muted, callback: v => v.toLocaleString('cs-CZ')} }, 4394 y2: { display: window.__tapView !== 'cement', position: 'right', beginAtZero: true, title: {display: true, text: 'TJ', color: muted}, grid: {display: false}, ticks: {color: muted, callback: v => v.toLocaleString('cs-CZ')} } 4395 } 4396 } 4397 }); 4398} 4399 4400function drawHeatFuelChart() { 4401 const el = document.getElementById('heat-fuel-chart'); if (!el) return; 4402 const muted = getCssVar('--text-muted') || '#71717a'; 4403 const strong = getCssVar('--text-strong') || '#09090b'; 4404 const bgCard = getCssVar('--bg-card') || '#fff'; 4405 const yr = window.__heatFuelYear || '2024'; 4406 const f = HEAT_FUEL_BY_YEAR[yr]; 4407 const d = { total_tj: f.total_tj, items: [ 4408 { label: 'HnÄdé uhlÃ', pct: f.hnede, color: HEAT_FUEL_COLORS.hnede }, 4409 { label: 'Zemnà plyn', pct: f.plyn, color: HEAT_FUEL_COLORS.plyn }, 4410 { label: 'Biomasa', pct: f.biomasa, color: paletteColor(HEAT_FUEL_COLORS.biomasa) }, 4411 { label: 'Äerné uhlÃ', pct: f.cerne, color: HEAT_FUEL_COLORS.cerne }, 4412 { label: 'Ostatnà (odpad, druhotnéâ¦)', pct: f.ostatni, color: HEAT_FUEL_COLORS.ostatni } 4413 ]}; 4414 const totalEl = document.getElementById('heat-fuel-total'); 4415 if (totalEl) totalEl.textContent = f.total_tj.toLocaleString('cs-CZ') + ' TJ (rok ' + yr + ')'; 4416 if (heatFuelChart) heatFuelChart.destroy(); 4417 heatFuelChart = new Chart(el.getContext('2d'), { 4418 type: 'doughnut', 4419 data: { labels: d.items.map(i => i.label), datasets: [{ data: d.items.map(i => i.pct), backgroundColor: d.items.map(i => i.color), borderColor: bgCard, borderWidth: 2 }] }, 4420 options: { responsive: true, maintainAspectRatio: false, 4421 plugins: { 4422 legend: { position: 'right', labels: { color: muted, font: {size: 12}, padding: 10, usePointStyle: true, pointStyle: 'rectRounded' } }, 4423 tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8, 4424 callbacks: { label: c => c.label + ': ' + c.parsed.toFixed(1).replace('.', ',') + ' % (' + Math.round(d.total_tj * c.parsed / 100).toLocaleString('cs-CZ') + ' TJ)' } } 4425 } } 4426 }); 4427} 4428function drawHeatPlantsChart() { 4429 const el = document.getElementById('heat-plants-chart'); if (!el) return; 4430 const muted = getCssVar('--text-muted') || '#71717a'; 4431 const border = getCssVar('--border') || '#e4e4e7'; 4432 const strong = getCssVar('--text-strong') || '#09090b'; 4433 const bgCard = getCssVar('--bg-card') || '#fff'; 4434 const accent = getCssVar('--accent') || '#dc2626'; 4435 const yr = window.__heatPlantsYear || '2024'; 4436 const prevYr = String(parseInt(yr, 10) - 1); 4437 const kraje = HEAT_KRAJE_BY_YEAR[yr]; 4438 const prevKraje = HEAT_KRAJE_BY_YEAR[prevYr] || null; 4439 const it = Object.keys(kraje).map(k => { 4440 const prev = prevKraje ? prevKraje[k] : null; 4441 return { label: k, tj: kraje[k], 4442 yoyDelta: prev ? kraje[k] - prev : null, 4443 yoyPct: prev ? Math.round((kraje[k] - prev) / prev * 1000) / 10 : null }; 4444 }).sort((a, b) => b.tj - a.tj); 4445 const total = it.reduce((s, i) => s + i.tj, 0); 4446 // Big ÄÃsla (stejný vzor jako sekce 14 â ceny tepla) 4447 const setTxt = (id, t) => { const e = document.getElementById(id); if (e) e.textContent = t; };
4448 setTxt('heat-plants-max', it[0].tj.toLocaleString('cs-CZ') + ' TJ'); 4449 setTxt('heat-plants-max-label', it[0].label + ' (nejvÄtšà v r. ' + yr + ')'); 4450 setTxt('heat-plants-min', it[it.length - 1].tj.toLocaleString('cs-CZ') + ' TJ'); 4451 setTxt('heat-plants-min-label', it[it.length - 1].label + ' (nejmenšà v r. ' + yr + ')'); 4452 setTxt('heat-plants-sum', total.toLocaleString('cs-CZ')); 4453 setTxt('heat-plants-sum-label', 'TJ celkem ÄR v r. ' + yr); 4454 const totalEl = document.getElementById('heat-plants-total'); 4455 if (totalEl) totalEl.textContent = total.toLocaleString('cs-CZ') + ' TJ (rok ' + yr + ')'; 4456 // Gradient dle hodnoty (stejnÄ jako sekce 14): nejvÃc = accent, nejménÄ = delta-down 4457 const max = it[0].tj, min = it[it.length - 1].tj; 4458 const colors = it.map(d => interpolateColor(getCssVar('--delta-down') || '#047857', accent, (d.tj - min) / (max - min))); 4459 if (heatPlantsChart) heatPlantsChart.destroy(); 4460 heatPlantsChart = new Chart(el.getContext('2d'), { 4461 type: 'bar', 4462 data: { labels: it.map(i => i.label), datasets: [{ label: 'RoÄnà dodávka tepla (TJ)', data: it.map(i => i.tj), 4463 backgroundColor: colors, borderColor: colors, borderWidth: 0, borderRadius: 6, borderSkipped: false }] }, 4464 options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, 4465 plugins: { 4466 legend: { display: false }, 4467 tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 12, cornerRadius: 8, displayColors: false, 4468 callbacks: { label: c => { 4469 const row = it[c.dataIndex]; 4470 const lines = [c.parsed.x.toLocaleString('cs-CZ') + ' TJ (' + yr + ') â ' + (c.parsed.x / total * 100).toFixed(1).replace('.', ',') + ' % ÄR']; 4471 if (row.yoyDelta !== null) { 4472 const sign = row.yoyDelta >= 0 ? '+' : ''; 4473 lines.push('vs ' + prevYr + ': ' + sign + row.yoyDelta.toLocaleString('cs-CZ') + ' TJ (' + sign + String(row.yoyPct).replace('.', ',') + ' %)'); 4474 } 4475 return lines; 4476 } } } 4477 }, 4478 scales: { 4479 x: { title: {display: true, text: 'TJ / rok', color: muted}, grid: {color: border}, ticks: {color: muted, callback: v => v.toLocaleString('cs-CZ')} }, 4480 y: { grid: {display: false}, ticks: {color: muted, font: {size: 11}} } 4481 } } 4482 }); 4483} 4484 4485const ENTSOE_FLOW_PALETTE = { 4486 'DE': '#000000', // Äerná (NÄmecko) 4487 'SK': '#1e40af', // modrá (Slovensko) 4488 'PL': '#dc2626', // Äervená (Polsko) 4489 'AT': '#dc2626' // Äerveno-bÃlá AT, ale modrá ji odlišà 4490}; 4491 4492function drawEntsoeFlowsChart() { 4493 if (!window.entsoeData?.flows_mw_hourly) return; 4494 const muted = getCssVar('--text-muted') || '#71717a'; 4495 const border = getCssVar('--border') || '#e4e4e7'; 4496 const strong = getCssVar('--text-strong') || '#09090b'; 4497 const bgCard = getCssVar('--bg-card') || '#fff'; 4498 const flows = entsoeData.flows_mw_hourly; 4499 const labels = Array.from({length: 24}, (_, i) => String(i).padStart(2,'0') + ':00'); 4500 const nbrs = ['DE', 'SK', 'PL', 'AT']; 4501 // Kontrastnà paleta, dobÅe Äitelná na svÄtlém i tmavém pozadà 4502 const nbrColors = { 4503 DE: '#f97316', // oranžová 4504 SK: '#3b82f6', // modrá 4505 PL: '#dc2626', // Äervená 4506 AT: '#16a34a' // zelená 4507 }; 4508 // Grouped bary per soused (jako sekce 10 v multi-year módu) â žádný pÅekryv barev. 4509 const datasets = nbrs.map(n => ({ 4510 label: 'CZ â ' + n, 4511 data: flows[n].net_export_cz, 4512 backgroundColor: nbrColors[n], 4513 borderRadius: 4, 4514 borderWidth: 0, 4515 order: 2 4516 })); 4517 // Net celkem CZ â tenká elegantnà linka v popÅedà 4518 const total = labels.map((_, h) => nbrs.reduce((s, n) => s + flows[n].net_export_cz[h], 0)); 4519 datasets.push({ 4520 label: 'Net celkem CZ', 4521 data: total, 4522 type: 'line', 4523 borderColor: strong, 4524 backgroundColor: strong, 4525 borderWidth: 2, 4526 pointRadius: 3, 4527 pointHoverRadius: 6,
4528 pointBackgroundColor: bgCard, 4529 pointBorderColor: strong, 4530 pointBorderWidth: 1.5, 4531 tension: 0.35, 4532 fill: false, 4533 order: 0 4534 }); 4535 if (entsoeFlowsChart) entsoeFlowsChart.destroy(); 4536 const ctx = document.getElementById('entsoe-flows-chart').getContext('2d'); 4537 entsoeFlowsChart = new Chart(ctx, { 4538 type: 'bar', 4539 data: { labels: labels, datasets: datasets }, 4540 options: { 4541 responsive: true, maintainAspectRatio: false, 4542 interaction: { mode: 'index', intersect: false }, 4543 plugins: { 4544 legend: { display: true, position: 'top', labels: { color: muted, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12, boxHeight: 8 } }, 4545 tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8, 4546 callbacks: { label: ctx => { 4547 const v = ctx.parsed.y; 4548 const dir = v >= 0 ? 'export' : 'import'; 4549 return ctx.dataset.label + ': ' + Math.round(Math.abs(v)).toLocaleString('cs-CZ') + ' MW (' + dir + ')'; 4550 } } } 4551 }, 4552 scales: { 4553 y: { 4554 title: {display: true, text: 'MW (+ export CZ / â import)', color: muted}, 4555 grid: { 4556 color: function(ctx) { return ctx.tick.value === 0 ? strong : border; }, 4557 lineWidth: function(ctx) { return ctx.tick.value === 0 ? 2 : 1; } 4558 }, 4559 ticks: {color: muted, callback: v => Math.round(v).toLocaleString('cs-CZ')} 4560 }, 4561 x: { grid: {display: false}, ticks: {color: muted, maxRotation: 0} } 4562 } 4563 } 4564 }); 4565 const dateEl = document.getElementById('entsoe-flows-date'); 4566 if (dateEl && entsoeData.date) { 4567 const p = entsoeData.date.split('-'); 4568 dateEl.textContent = parseInt(p[2], 10) + '. ' + parseInt(p[1], 10) + '. ' + p[0]; 4569 } 4570} 4571 4572// ===== Sekce 19: Ceny regulaÄnÃch záloh (FCR/aFRR/mFRR) â ENTSO-E A15 ===== 4573const BAL_SERIES = [ 4574 { key: 'FCR_sym', label: 'FCR (sym.)', color: '#7c3aed' }, 4575 { key: 'aFRR_up', label: 'aFRR â', color: '#dc2626' }, 4576 { key: 'aFRR_down', label: 'aFRR â', color: '#fb923c' }, 4577 { key: 'mFRR_up', label: 'mFRR â', color: '#0ea5e9' }, 4578 { key: 'mFRR_down', label: 'mFRR â', color: '#16a34a' } 4579]; 4580const balChartState = { range: '3m', metric: 'avg', hidden: {} }; 4581let balChart = null; 4582 4583function balRangeStartMs(range, lastMs) { 4584 if (range === 'all') return -Infinity; 4585 const d = new Date(lastMs); 4586 d.setMonth(d.getMonth() - ({ '1m': 1, '3m': 3, '6m': 6, '12m': 12 }[range] || 3)); 4587 return d.getTime(); 4588} 4589 4590function drawBalancingChart() { 4591 const cv = document.getElementById('bal-chart'); 4592 if (!cv || typeof Chart === 'undefined') return; 4593 const data = window.balancingData || {}; 4594 let lastMs = 0; 4595 BAL_SERIES.forEach(s => (data[s.key] || []).forEach(p => { const t = new Date(p.date).getTime(); if (t > lastMs) lastMs = t; })); 4596 if (!lastMs) lastMs = Date.now(); 4597 const startMs = balRangeStartMs(balChartState.range, lastMs); 4598 const metric = balChartState.metric; 4599 const datasets = BAL_SERIES.map(s => ({ 4600 label: s.label, 4601 data: (data[s.key] || []) 4602 .filter(p => new Date(p.date).getTime() >= startMs && p[metric] != null) 4603 .map(p => ({ x: new Date(p.date).getTime(), y: p[metric], mw: p.mw })), 4604 borderColor: s.color, backgroundColor: s.color + '22', 4605 borderWidth: 2, pointRadius: 0, tension: 0.15, spanGaps: true, parsing: false, 4606 hidden: !!balChartState.hidden[s.key] 4607 })); 4608 const muted = getCssVar('--text-muted') || '#71717a'; 4609 const strong = getCssVar('--text-strong') || '#09090b'; 4610 const border = getCssVar('--border') || '#e4e4e7'; 4611 const bgCard = getCssVar('--bg-card') || '#fff'; 4612 if (balChart) balChart.destroy(); 4613 balChart = new Chart(cv.getContext('2d'), { 4614 type: 'line', 4615 data: { datasets }, 4616 options: { 4617 responsive: true, maintainAspectRatio: false, 4618 interaction: { mode: 'nearest', intersect: false }, 4619 plugins: { 4620 legend: { display: false }, 4621 tooltip: { 4622 backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, borderColor: border, 4623 padding: 12, cornerRadius: 8, boxPadding: 4, 4624 callbacks: { 4625 title: items => items.length ? new Date(items[0].parsed.x).toLocaleDateString('cs-CZ') : '', 4626 label: ctx => { 4627 const mw = ctx.raw && ctx.raw.mw != null ? ', ' + Math.round(ctx.raw.mw) + ' MW' : ''; 4628 return ctx.dataset.label + ': ' + (ctx.parsed.y == null ? 'â' : ctx.parsed.y.toFixed(2).replace('.', ',') + ' EUR/MW' + mw); 4629 } 4630 } 4631 } 4632 }, 4633 scales: { 4634 y: { type: 'linear', beginAtZero: true, title: { display: true, text: 'EUR/MW', color: muted, font: { size: 11 } }, 4635 grid: { color: border, lineWidth: 1 }, ticks: { color: muted, font: { size: 11 } } }, 4636 x: { type: 'linear', bounds: 'data', grid: { display: false }, afterDataLimits: captureXDataLimits, afterBuildTicks: calendarXTicks, 4637 ticks: { color: muted, autoSkip: false, maxRotation: 0, font: { size: 11 },
4638 callback: adaptiveXTick } } 4639 } 4640 } 4641 }); 4642 renderBalLegend(); 4643} 4644 4645function renderBalLegend() { 4646 const el = document.getElementById('bal-legend'); 4647 if (!el) return; 4648 el.innerHTML = BAL_SERIES.map(s => { 4649 const off = balChartState.hidden[s.key] ? 'opacity:.35;' : ''; 4650 return '<span style="cursor:pointer;display:inline-flex;align-items:center;gap:5px;margin-right:14px;font-size:13px;' + off + '"' 4651 + ' onclick="toggleBalSeries(\'' + s.key + '\')">' 4652 + '<span style="display:inline-block;width:11px;height:11px;border-radius:3px;background:' + s.color + '"></span>' 4653 + s.label + '</span>'; 4654 }).join(''); 4655} 4656function toggleBalSeries(key) { balChartState.hidden[key] = !balChartState.hidden[key]; drawBalancingChart(); } 4657function setBalRange(btn, range) { 4658 balChartState.range = range; 4659 if (btn && btn.parentElement) btn.parentElement.querySelectorAll('[data-balrange]').forEach(b => b.classList.toggle('active', b === btn)); 4660 drawBalancingChart(); 4661} 4662function setBalMetric(btn, metric) { 4663 balChartState.metric = metric; 4664 if (btn && btn.parentElement) btn.parentElement.querySelectorAll('[data-balmetric]').forEach(b => b.classList.toggle('active', b === btn)); 4665 drawBalancingChart(); 4666} 4667 4668// ===== Sekce 20: ZásobnÃky plynu v EvropÄ (AGSI) â choropleth mapa + dennà vývoj ===== 4669const EU_STO_SERIES = [ 4670 { code: 'EU', label: 'EU', color: '#0ea5e9' }, 4671 { code: 'CZ', label: 'Äesko', color: '#dc2626' }, 4672 { code: 'DE', label: 'NÄmecko', color: '#f59e0b' }, 4673 { code: 'FR', label: 'Francie', color: '#7c3aed' }, 4674 { code: 'IT', label: 'Itálie', color: '#16a34a' }, 4675 { code: 'NL', label: 'Nizozemsko', color: '#0284c7' }, 4676 { code: 'AT', label: 'Rakousko', color: '#db2777' }, 4677 { code: 'PL', label: 'Polsko', color: '#65a30d' }, 4678 { code: 'ES', label: 'Å panÄlsko', color: '#ea580c' }, 4679 { code: 'HU', label: 'MaÄarsko', color: '#0891b2' } 4680]; 4681const euStoState = { range: '6m', hidden: {} }; 4682EU_STO_SERIES.forEach(s => { if (!['EU', 'CZ', 'DE'].includes(s.code)) euStoState.hidden[s.code] = true; }); 4683let euStoTrendChart = null, euMapChart = null, __euGeo = null; 4684 4685function euStoRangeStart(range, lastMs) { 4686 if (range === 'all') return -Infinity; 4687 const d = new Date(lastMs); 4688 d.setMonth(d.getMonth() - ({ '3m': 3, '6m': 6, '12m': 12 }[range] || 6)); 4689 return d.getTime(); 4690} 4691 4692function drawEuStorageYears() { 4693 const cv = document.getElementById('eu-storage-trend'); if (!cv || typeof Chart === 'undefined') return; 4694 const hist = window.euStorageHistory || {}; 4695 const legEl = document.getElementById('eu-storage-legend'); if (legEl) legEl.innerHTML = '<span style="font-size:12.5px;color:var(--text-muted,#71717a)">EU agregát â % naplnÄnà po kalendáÅnÃch týdnech</span>'; 4696 const muted = getCssVar('--text-muted') || '#71717a', border = getCssVar('--border') || '#e4e4e7', strong = getCssVar('--text-strong') || '#09090b', bgCard = getCssVar('--bg-card') || '#fff'; 4697 const yc = (typeof YEAR_COLORS !== 'undefined') ? YEAR_COLORS : {}; 4698 const pc = (typeof paletteColor === 'function') ? paletteColor : (x => x); 4699 const datasets = Object.keys(hist).sort().map(year => ({ 4700 label: year, data: hist[year].map(p => ({ x: p.week, y: p.value })), 4701 borderColor: pc(yc[year]) || undefined, backgroundColor: 'transparent', 4702 pointRadius: 0, pointHoverRadius: 4, borderWidth: year === '2026' ? 2.5 : 1.8, tension: 0.35 4703 })); 4704 const fy = Object.keys(hist).filter(y => y >= '2020' && y <= '2025'); 4705 if (fy.length) { 4706 const sum = {}, n = {}; 4707 fy.forEach(y => hist[y].forEach(p => { const w = Math.round(p.week); sum[w] = (sum[w] || 0) + p.value; n[w] = (n[w] || 0) + 1; })); 4708 datasets.push({ label: 'PrůmÄr 2020â2025', data: Object.keys(sum).map(Number).sort((a, b) => a - b).map(w => ({ x: w, y: Math.round(sum[w] / n[w] * 10) / 10 })), borderColor: muted, backgroundColor: 'transparent', borderDash: [6, 4], pointRadius: 0, borderWidth: 2, tension: 0.35 }); 4709 } 4710 if (euStoTrendChart) euStoTrendChart.destroy(); 4711 euStoTrendChart = new Chart(cv.getContext('2d'), { 4712 type: 'line', data: { datasets }, 4713 options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'nearest', intersect: false }, 4714 plugins: { legend: { display: true, position: 'top', labels: { color: muted, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12, boxHeight: 8, padding: 10, font: { size: 11 } } }, 4715 tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8, callbacks: { title: items => 'Týden ' + Math.round(items[0].parsed.x), label: it => it.dataset.label + ': ' + it.parsed.y.toFixed(1).replace('.', ',') + ' %' } } }, 4716 scales: { x: { type: 'linear', min: 1, max: 53.2, title: { display: true, text: 'KalendáÅnà týden', color: muted, font: { size: 11 } }, ticks: { stepSize: 4, color: muted, font: { size: 11 } }, grid: { display: false } }, 4717 y: { min: 0, max: 100, title: { display: true, text: '% naplnÄnÃ', color: muted, font: { size: 11 } }, grid: { color: border }, ticks: { color: muted, font: { size: 11 } } } } } 4718 }); 4719} 4720 4721function drawEuStorageTrend() { 4722 const cv = document.getElementById('eu-storage-trend'); 4723 if (!cv || typeof Chart === 'undefined') return; 4724 if (euStoState.range === 'years') return drawEuStorageYears(); 4725 const series = window.euStorageSeries || {}; 4726 let lastMs = 0; 4727 EU_STO_SERIES.forEach(s =>
4727 (series[s.code] || []).forEach(p => { const t = new Date(p.date).getTime(); if (t > lastMs) lastMs = t; })); 4728 if (!lastMs) lastMs = Date.now(); 4729 const startMs = euStoRangeStart(euStoState.range, lastMs); 4730 const datasets = EU_STO_SERIES.map(s => ({ 4731 label: s.label, 4732 data: (series[s.code] || []).filter(p => new Date(p.date).getTime() >= startMs && p.full != null) 4733 .map(p => ({ x: new Date(p.date).getTime(), y: p.full, twh: p.twh })), 4734 borderColor: s.color, backgroundColor: s.color + '22', 4735 borderWidth: s.code === 'EU' ? 3 : 1.8, pointRadius: 0, tension: 0.15, spanGaps: true, parsing: false, 4736 hidden: !!euStoState.hidden[s.code] 4737 })); 4738 const muted = getCssVar('--text-muted') || '#71717a', strong = getCssVar('--text-strong') || '#09090b', 4739 border = getCssVar('--border') || '#e4e4e7', bgCard = getCssVar('--bg-card') || '#fff'; 4740 if (euStoTrendChart) euStoTrendChart.destroy(); 4741 euStoTrendChart = new Chart(cv.getContext('2d'), { 4742 type: 'line', data: { datasets }, 4743 options: { 4744 responsive: true, maintainAspectRatio: false, interaction: { mode: 'nearest', intersect: false }, 4745 plugins: { 4746 legend: { display: false }, 4747 tooltip: { 4748 backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, borderColor: border, padding: 12, cornerRadius: 8, boxPadding: 4, 4749 callbacks: { 4750 title: items => items.length ? new Date(items[0].parsed.x).toLocaleDateString('cs-CZ') : '', 4751 label: ctx => ctx.dataset.label + ': ' + (ctx.parsed.y == null ? 'â' : ctx.parsed.y.toFixed(1).replace('.', ',') + ' %') 4752 + (ctx.raw && ctx.raw.twh != null ? ' (' + ctx.raw.twh.toFixed(1) + ' TWh)' : '') 4753 } 4754 } 4755 }, 4756 scales: { 4757 y: { type: 'linear', min: 0, max: 100, title: { display: true, text: '% naplnÄnÃ', color: muted, font: { size: 11 } }, 4758 grid: { color: border, lineWidth: 1 }, ticks: { color: muted, font: { size: 11 } } }, 4759 x: { type: 'linear', bounds: 'data', grid: { display: false }, afterDataLimits: captureXDataLimits, afterBuildTicks: calendarXTicks, 4760 ticks: { color: muted, autoSkip: false, maxRotation: 0, font: { size: 11 }, 4761 callback: adaptiveXTick } } 4762 } 4763 } 4764 }); 4765 renderEuStoLegend(); 4766} 4767 4768function renderEuStoLegend() { 4769 const el = document.getElementById('eu-storage-legend'); 4770 if (!el) return; 4771 el.innerHTML = EU_STO_SERIES.map(s => { 4772 const off = euStoState.hidden[s.code] ? 'opacity:.35;' : ''; 4773 return '<span style="cursor:pointer;display:inline-flex;align-items:center;gap:5px;margin-right:12px;font-size:12.5px;' + off + '"' 4774 + ' onclick="toggleEuSto(\'' + s.code + '\')">' 4775 + '<span style="display:inline-block;width:11px;height:11px;border-radius:3px;background:' + s.color + '"></span>' + s.label + '</span>'; 4776 }).join(''); 4777} 4778function toggleEuSto(code) { euStoState.hidden[code] = !euStoState.hidden[code]; drawEuStorageTrend(); } 4779function setEuStoRange(btn, range) { 4780 euStoState.range = range; 4781 if (btn && btn.parentElement) btn.parentElement.querySelectorAll('[data-eustorange]').forEach(b => b.classList.toggle('active', b === btn)); 4782 drawEuStorageTrend(); 4783} 4784 4785async function drawEuStorageMap() { 4786 const cv = document.getElementById('eu-storage-map'); 4787 if (!cv || typeof Chart === 'undefined') return; 4788 if (!Chart.registry.controllers.get('choropleth')) { console.warn('chartjs-chart-geo nenaÄten'); return; } 4789 try { 4790 if (!__euGeo) { 4791 const r = await fetch('https://cdn.jsdelivr.net/gh/leakyMirror/map-of-europe@27a335110674ae5b01a84d3501b227e661beea2b/GeoJSON/europe.geojson'); 4792 __euGeo = await r.json(); 4793 } 4794 const latest = window.euStorageLatest || {}; 4795 const feats = __euGeo.features.filter(f => !['AZ','AM','GE','CY','IS','TR','RU','IL'].includes(f.properties.ISO2)); 4796 const muted = getCssVar('--text-muted') || '#71717a', strong = getCssVar('--text-strong') || '#09090b', 4797 bgCard = getCssVar('--bg-card') || '#fff', border = getCssVar('--border') || '#e4e4e7'; 4798 const ramp = t => (t < 0.5 ? interpolateColor('#dc2626', '#f59e0b', t * 2) : interpolateColor('#f59e0b', '#16a34a', (t - 0.5) * 2)); 4799 if (euMapChart) euMapChart.destroy(); 4800 euMapChart = new Chart(cv.getContext('2d'), { 4801 type: 'choropleth', 4802 data: { 4803 labels: feats.map(f => f.properties.NAME), 4804 datasets: [{ 4805 outline: feats, showOutline: true, 4806 data: feats.map(f => ({ feature: f, value: latest[f.properties.ISO2] ? latest[f.properties.ISO2].full : null })), 4807 borderColor: border, borderWidth: 0.4 4808 }] 4809 }, 4810 options: { 4811 responsive: true, maintainAspectRatio: false, 4812 plugins: { 4813 legend: { display: false }, 4814 tooltip: { 4815 backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, borderColor: border, padding: 10, cornerRadius: 8, 4816 callbacks: { 4817 title: () => '', 4818 label: ctx => { 4819 const f = ctx.raw.feature, iso = f.properties.ISO2, o = latest[iso], v = ctx.raw.value; 4820 return v == null ? f.properties.NAME + ': bez dat' 4821 : f.properties.NAME + ': ' + v.toFixed(1).replace('.', ',') + ' %' + (o && o.twh != null ? ' (' + o.twh.toFixed(1) + ' TWh)' : ''); 4822 } 4823 } 4824 } 4825 }, 4826 scales: { 4827 projection: { axis: 'x', projection: 'mercator' }, 4828 color: { axis: 'x', min: 0, max: 100, interpolate: ramp, 4829 legend: { position: 'bottom-right', align: 'bottom', length: 120, width: 8 }, 4830 ticks: { color: muted } } 4831 } 4832 } 4833 }); 4834 } catch (e) { console.error('eu map failed', e); } 4835} 4836 4837function drawEuStorage() { drawEuStorageMap(); drawEuStorageTrend(); } 4838 4839// ===== Sekce 21: Skladba dovozu plynu do EU (ENTSOG + ALSI) ===== 4840const IMPORT_SERIES = [ 4841 { key: 'Norway', label: 'Norsko', color: '#0ea5e9' }, 4842 { key: 'LNG', label: 'LNG', color: '#7c3aed' }, 4843 { key: 'Russia', label: 'Rusko', color: '#dc2626' }, 4844 { key: 'Algeria', label: 'AlžÃrsko', color: '#f59e0b' }, 4845 { key: 'Azerbaijan', label: 'Ãzerbájdžán', color: '#16a34a' }, 4846 { key: 'Libya', label: 'Libye', color: '#ca8a04' }, 4847 { key: 'Turkey', label: 'Turecko', color: '#0891b2' }, 4848 { key: 'Ukraine', label: 'Ukrajina', color: '#64748b' }, 4849 { key: 'Serbia', label: 'Srbsko', color: '#db2777' } 4850]; 4851const impState = { range: '6m', hidden: {} }; 4852let importsChart = null; 4853function impRangeStart(range, lastMs) { if (range === 'all') return -Infinity; const d = new Date(lastMs); d.setMonth(d.getMonth() - ({ '3m': 3, '6m': 6, '12m': 12 }[range] || 6)); return d.getTime(); } 4854function drawImportsChart() { 4855 const cv = document.getElementById('imports-chart'); if (!cv || typeof Chart === 'undefined') return; 4856 const data = window.euImports || {}; 4857 const allD = new Set(); IMPORT_SERIES.forEach(s =>
4857 (data[s.key] || []).forEach(p => allD.add(p.date))); 4858 let dates = Array.from(allD).sort(); 4859 if (!dates.length) { if (importsChart) { importsChart.destroy(); importsChart = null; } return; } 4860 const lastMs = new Date(dates[dates.length - 1]).getTime(); 4861 const startMs = impRangeStart(impState.range, lastMs); 4862 dates = dates.filter(d => new Date(d).getTime() >= startMs); 4863 const dset = new Set(dates); 4864 const datasets = IMPORT_SERIES.map(s => { const m = {}; (data[s.key] || []).forEach(p => { if (dset.has(p.date)) m[p.date] = p.gwh; }); 4865 return { label: s.label, data: dates.map(d => m[d] == null ? 0 : m[d]), borderColor: s.color, backgroundColor: s.color + 'cc', borderWidth: 1, pointRadius: 0, fill: true, tension: 0.1, hidden: !!impState.hidden[s.key] }; }); 4866 const muted = getCssVar('--text-muted') || '#71717a', strong = getCssVar('--text-strong') || '#09090b', border = getCssVar('--border') || '#e4e4e7', bgCard = getCssVar('--bg-card') || '#fff'; 4867 if (importsChart) importsChart.destroy(); 4868 importsChart = new Chart(cv.getContext('2d'), { 4869 type: 'line', data: { labels: dates, datasets }, 4870 options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, 4871 plugins: { legend: { display: false }, 4872 tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, borderColor: border, padding: 10, cornerRadius: 8, 4873 callbacks: { title: items => items.length ? new Date(items[0].label).toLocaleDateString('cs-CZ') : '', label: ctx => ctx.dataset.label + ': ' + Math.round(ctx.parsed.y) + ' GWh/d' } } }, 4874 scales: { y: { stacked: true, beginAtZero: true, title: { display: true, text: 'GWh/d', color: muted, font: { size: 11 } }, grid: { color: border }, ticks: { color: muted, font: { size: 11 } } }, 4875 x: { stacked: true, grid: { display: false }, ticks: { color: muted, maxTicksLimit: 8, autoSkip: true, font: { size: 11 }, callback: function (v) { return new Date(this.getLabelForValue(v)).toLocaleDateString('cs-CZ', { month: 'short', year: '2-digit' }); } } } } } 4876 }); 4877 renderImpLegend(); 4878} 4879function renderImpLegend() { const el = document.getElementById('imports-legend'); if (!el) return; 4880 el.innerHTML = IMPORT_SERIES.map(s => { const off = impState.hidden[s.key] ? 'opacity:.35;' : ''; return '<span style="cursor:pointer;display:inline-flex;align-items:center;gap:5px;margin-right:12px;font-size:12.5px;' + off + '" onclick="toggleImp(\'' + s.key + '\')"><span style="display:inline-block;width:11px;height:11px;border-radius:3px;background:' + s.color + '"></span>' + s.label + '</span>'; }).join(''); } 4881function toggleImp(k) { impState.hidden[k] = !impState.hidden[k]; drawImportsChart(); } 4882function setImpRange(btn, range) { impState.range = range; if (btn && btn.parentElement) btn.parentElement.querySelectorAll('[data-imprange]').forEach(b => b.classList.toggle('active', b === btn)); drawImportsChart(); } 4883 4884// Jediné tlaÄÃtko âAktualizovat data" sjednocuje obÄ Äinnosti: 4885// 1) vynutà nový výpoÄet dat (Edge Function âtrigger-pipeline" â GitHub Actions), 4886// 2) poÄká na dokonÄenà bÄhu a pak pÅenaÄte report s Äerstvými daty. 4887// Když vynucenà nenà dostupné (token/funkce/sÃÅ¥), spadne na prostý reload z DB â tlaÄÃtko 4888// tedy VŽDY udÄlá nÄco užiteÄného. GitHub token držà server, ne klientské HTML. 4889// Trvalá cache stažených tabulek v IndexedDB (pÅežije navigaci mezi listy; TTL 30 min). 4890const __idbCache = (() => { 4891 let dbp = null; 4892 function open() { 4893 if (dbp) return dbp; 4894 dbp = new Promise((res, rej) => { 4895 let r; try { r = indexedDB.open('erpcache', 1); } catch (e) { return rej(e); } 4896 r.onupgradeneeded = () => { try { r.result.createObjectStore('t'); } catch (e) {} }; 4897 r.onsuccess = () => res(r.result); 4898 r.onerror = () => rej(r.error); 4899 }); 4900 return dbp; 4901 } 4902 return { 4903 async get(k) { try { const db = await open(); return await new Promise((res) => { const rq = db.transaction('t').objectStore('t').get(k); rq.onsuccess = () => res(rq.result || null); rq.onerror = () => res(null); }); } catch (e) { return null; } },
4904 set(k, v) { open().then(db => { try { db.transaction('t', 'readwrite').objectStore('t').put(v, k); } catch (e) {} }).catch(() => {}); }, 4905 async clear() { try { const db = await open(); db.transaction('t', 'readwrite').objectStore('t').clear(); } catch (e) {} } 4906 }; 4907})(); 4908 4909async function refreshData() { 4910 try { await __idbCache.clear(); } catch (e) {} 4911 const btn = document.getElementById('refresh-btn'); 4912 const banner = document.getElementById('status-banner'); 4913 const fnUrl = window.__SUPABASE_URL + '/functions/v1/trigger-pipeline'; 4914 // POZOR: funkce už nepÅijÃmá pouhý anon klÃÄ (kdokoli s URL by jinak mohl spouÅ¡tÄt bÄhy pipeline). 4915 // PosÃláme token pÅihlášeného uživatele; anon klÃÄ zůstává jen jako apikey. 4916 const { data: { session } } = await supaClient().auth.getSession(); 4917 if (!session) { alert('Pro aktualizaci dat je potÅeba být pÅihlášen.'); return; } 4918 const authHdr = { 'Authorization': 'Bearer ' + session.access_token, 4919 'apikey': window.__SUPABASE_ANON, 'Content-Type': 'application/json' }; 4920 const setBtn = (t) => { if (btn) btn.innerHTML = '<span>â»</span><span>' + t + '</span>'; }; 4921 const setBanner = (cls, html) => { if (banner) { banner.className = 'status-banner ' + cls; banner.innerHTML = html; } }; 4922 const reload = () => { 4923 const url = new URL(window.location.href); 4924 url.searchParams.set('_t', Date.now()); 4925 window.location.replace(url.toString()); 4926 }; 4927 if (btn) btn.disabled = true; 4928 setBtn('Aktualizujiâ¦'); 4929 setBanner('info', 'SpouÅ¡tÃm aktualizaci datâ¦'); 4930 4931 // 1) Zkus vynutit nový bÄh pipeline. Když to nejde, aspoÅ pÅenaÄti aktuálnà data z DB. 4932 let trig; 4933 try { 4934 const r = await fetch(fnUrl, { method: 'POST', headers: authHdr }); 4935 if (!r.ok) { setBanner('info', 'NaÄÃtám aktuálnà data z databázeâ¦'); return setTimeout(reload, 500); } 4936 trig = await r.json(); 4937 } catch (e) { 4938 setBanner('info', 'NaÄÃtám aktuálnà data z databázeâ¦'); 4939 return setTimeout(reload, 500); 4940 } 4941 if (trig && trig.mode === 'recent') { 4942 setBanner('success', 'â Data byla právÄ aktualizována, naÄÃtámâ¦'); 4943 return setTimeout(reload, 700); 4944 } 4945 4946 // 2) BÄh bÄžà â Äekej na dokonÄenà a pak pÅenaÄti report. 4947 const sinceMs = (trig && trig.watch_since_ms) || 0; 4948 const startedAt = Date.now(); 4949 const MAX_MS = 5 * 60 * 1000; 4950 setBanner('info', 'Aktualizuji data â nový výpoÄet bÄžà (obvykle ~1â2 min). Stránka se pak sama obnovÃ.'); 4951 while (Date.now() - startedAt < MAX_MS) { 4952 await new Promise((res) => setTimeout(res, 12000)); 4953 setBtn('Aktualizuji⦠' + Math.round((Date.now() - startedAt) / 1000) + ' s'); 4954 let st = null; 4955 try { 4956 const sr = await fetch(fnUrl, { method: 'POST', headers: authHdr, body: JSON.stringify({ action: 'status' }) }); 4957 if (sr.ok) st = await sr.json(); 4958 } catch (_) { /* ignoruj pÅechodné chyby a zkus znovu */ } 4959 if (st && st.status === 'completed' && st.created_at && Date.parse(st.created_at) >= sinceMs) { 4960 setBanner('success', 'â Hotovo, naÄÃtám Äerstvá dataâ¦'); 4961 return setTimeout(reload, 800); 4962 } 4963 } 4964 setBanner('info', 'VýpoÄet trvá déle než obvykle; naÄÃtám zatÃm poslednà dataâ¦'); 4965 reload(); 4966} 4967</script>
4967 4968 4969</body> 4970</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.