From 71bcc754986159fdacd3f3ba606f0af281d32fee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20Fefli=C5=84ski?= Date: Mon, 2 Feb 2026 14:51:21 +0100 Subject: [PATCH] More charts --- assets/css/admin.css | 21 ++ moje-statystyki.php | 558 +++++++++++++++++++++++++++---------------- 2 files changed, 368 insertions(+), 211 deletions(-) diff --git a/assets/css/admin.css b/assets/css/admin.css index 9e63ace..fc1bb6a 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -37,3 +37,24 @@ flex: 1 1 48%; /* Pozwala na elastyczne dopasowanie, z bazową szerokością 48% */ min-width: 300px; /* Zapobiega zbytniemu ściskaniu kolumn na mniejszych ekranach */ } + +/* Chart Controls */ +.mystat-chart-controls { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + margin-bottom: 15px; + border-bottom: 1px solid #ddd; + padding-bottom: 10px; +} + +.mystat-chart-tabs.nav-tab-wrapper { + border-bottom: none; + margin-bottom: 0; +} + +.mystat-xaxis-switcher { + padding-top: 5px; + white-space: nowrap; +} diff --git a/moje-statystyki.php b/moje-statystyki.php index d5a34b7..2b148f4 100644 --- a/moje-statystyki.php +++ b/moje-statystyki.php @@ -774,113 +774,125 @@ function mystat_yearly_summary_page() { * @return array An array containing 'points' for the map and 'elevation_profile'. */ function mystat_parse_gpx_data( $gpx_url ) { - if ( empty( $gpx_url ) ) { - return []; - } + if ( empty( $gpx_url ) ) { + return []; + } - $response = wp_remote_get( $gpx_url, [ 'timeout' => 20 ] ); + $response = wp_remote_get( $gpx_url, [ 'timeout' => 20 ] ); - if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) { - return []; - } + if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) { + return []; + } - $gpx_content = wp_remote_retrieve_body( $response ); - if ( empty( $gpx_content ) ) { - return []; - } + $gpx_content = wp_remote_retrieve_body( $response ); + if ( empty( $gpx_content ) ) { + return []; + } - // --- Privacy Zone --- - $privacy_options = get_option( 'mystat_privacy_options' ); - $privacy_enabled = false; - $privacy_center_lat = 0; - $privacy_center_lon = 0; - $privacy_radius_km = 0; + // --- Privacy Zone --- + $privacy_options = get_option( 'mystat_privacy_options' ); + $privacy_enabled = ! empty( $privacy_options['latitude'] ) && ! empty( $privacy_options['longitude'] ) && ! empty( $privacy_options['radius'] ); + $privacy_center_lat = $privacy_enabled ? (float) $privacy_options['latitude'] : 0; + $privacy_center_lon = $privacy_enabled ? (float) $privacy_options['longitude'] : 0; + $privacy_radius_km = $privacy_enabled ? ( (int) $privacy_options['radius'] ) / 1000 : 0; // Convert meters to km - if ( ! empty( $privacy_options['latitude'] ) && ! empty( $privacy_options['longitude'] ) && ! empty( $privacy_options['radius'] ) ) { - $privacy_enabled = true; - $privacy_center_lat = (float) $privacy_options['latitude']; - $privacy_center_lon = (float) $privacy_options['longitude']; - $privacy_radius_km = ( (int) $privacy_options['radius'] ) / 1000; // Convert meters to km - } + libxml_use_internal_errors( true ); + $gpx = simplexml_load_string( $gpx_content ); + libxml_clear_errors(); - libxml_use_internal_errors( true ); - $gpx = simplexml_load_string( $gpx_content ); - libxml_clear_errors(); + if ( $gpx === false ) { + return []; + } - if ( $gpx === false ) { - return []; - } + // Use XPath to be more robust against different GPX structures/namespaces + $gpx->registerXPathNamespace( 'gpx', 'http://www.topografix.com/GPX/1/1' ); + $trackpoints = $gpx->xpath( '//gpx:trkpt' ); + if ( empty( $trackpoints ) ) { + $trackpoints = $gpx->xpath( '//trkpt' ); // Fallback for files without namespace + } - $track_data = [ - 'points' => [], - 'elevation_profile' => [], - ]; - $raw_points = []; + $raw_points = []; + $start_time = null; - // Extract raw points with lat, lon, ele - if ( isset( $gpx->trk ) ) { - foreach ( $gpx->trk->trkseg as $trkseg ) { - foreach ( $trkseg->trkpt as $trkpt ) { - if ( isset( $trkpt['lat'], $trkpt['lon'] ) ) { - $raw_points[] = [ - 'lat' => (float) $trkpt['lat'], - 'lon' => (float) $trkpt['lon'], - 'ele' => isset( $trkpt->ele ) ? (float) $trkpt->ele : null, - ]; - } - } - } - } + foreach ( $trackpoints as $trkpt ) { + if ( isset( $trkpt['lat'], $trkpt['lon'] ) ) { + $extensions = $trkpt->extensions ? $trkpt->extensions->children( 'gpxtpx', true ) : null; + $time = isset( $trkpt->time ) ? strtotime( (string) $trkpt->time ) : null; + if ( $time && is_null( $start_time ) ) { + $start_time = $time; + } - if ( empty( $raw_points ) ) { - return $track_data; - } + $hr_val = ( $extensions && isset( $extensions->TrackPointExtension->hr ) ) ? (int) $extensions->TrackPointExtension->hr : null; + $cad_val = ( $extensions && isset( $extensions->TrackPointExtension->cad ) ) ? (int) $extensions->TrackPointExtension->cad : null; - // Process raw points to calculate profile - $cumulative_distance = 0; - $elevation_profile = []; - $map_points = []; + $raw_points[] = [ + 'lat' => (float) $trkpt['lat'], + 'lon' => (float) $trkpt['lon'], + 'ele' => isset( $trkpt->ele ) ? (float) $trkpt->ele : null, + 'time_offset' => $start_time && $time ? $time - $start_time : null, + 'hr' => $hr_val, + 'cad' => $cad_val, + ]; + } + } - $haversine = function( $lat1, $lon1, $lat2, $lon2 ) { - $earth_radius = 6371; // in km - $dLat = deg2rad( $lat2 - $lat1 ); - $dLon = deg2rad( $lon2 - $lon1 ); - $a = sin( $dLat / 2 ) * sin( $dLat / 2 ) + - cos( deg2rad( $lat1 ) ) * cos( deg2rad( $lat2 ) ) * - sin( $dLon / 2 ) * sin( $dLon / 2 ); - $c = 2 * atan2( sqrt( $a ), sqrt( 1 - $a ) ); - return $earth_radius * $c; - }; + if ( empty( $raw_points ) ) { + return []; + } - foreach ( $raw_points as $i => $point ) { - // Check if point is inside privacy zone - $is_in_privacy_zone = false; - if ( $privacy_enabled ) { - $distance_from_center = $haversine( $privacy_center_lat, $privacy_center_lon, $point['lat'], $point['lon'] ); - if ( $distance_from_center <= $privacy_radius_km ) { - $is_in_privacy_zone = true; - } - } + // Process raw points to calculate profiles + $map_points = []; + $profiles = [ 'distance' => [], 'time' => [], 'elevation' => [], 'speed' => [], 'hr' => [], 'cadence' => [] ]; + $cumulative_distance = 0; - // Only process points outside the privacy zone - if ( ! $is_in_privacy_zone ) { - $map_points[] = [ $point['lat'], $point['lon'] ]; + $haversine = function( $lat1, $lon1, $lat2, $lon2 ) { + $earth_radius = 6371; // in km + $dLat = deg2rad( $lat2 - $lat1 ); + $dLon = deg2rad( $lon2 - $lon1 ); + $a = sin( $dLat / 2 ) * sin( $dLat / 2 ) + cos( deg2rad( $lat1 ) ) * cos( deg2rad( $lat2 ) ) * sin( $dLon / 2 ) * sin( $dLon / 2 ); + $c = 2 * atan2( sqrt( $a ), sqrt( 1 - $a ) ); + return $earth_radius * $c; + }; - if ( $i > 0 ) { - $prev_point = $raw_points[ $i - 1 ]; - $cumulative_distance += $haversine( $prev_point['lat'], $prev_point['lon'], $point['lat'], $point['lon'] ); - } + foreach ( $raw_points as $i => $point ) { + $is_in_privacy_zone = false; + if ( $privacy_enabled ) { + $distance_from_center = $haversine( $privacy_center_lat, $privacy_center_lon, $point['lat'], $point['lon'] ); + if ( $distance_from_center <= $privacy_radius_km ) { + $is_in_privacy_zone = true; + } + } - if ( ! is_null( $point['ele'] ) ) { - $elevation_profile[] = [ 'distance' => round( $cumulative_distance, 3 ), 'elevation' => round( $point['ele'], 2 ) ]; - } - } - } + if ( ! $is_in_privacy_zone ) { + $map_points[] = [ $point['lat'], $point['lon'] ]; + $speed = null; - $track_data['points'] = $map_points; - $track_data['elevation_profile'] = $elevation_profile; + if ( $i > 0 ) { + $prev_point = $raw_points[ $i - 1 ]; + $distance_delta = $haversine( $prev_point['lat'], $prev_point['lon'], $point['lat'], $point['lon'] ); // km + $cumulative_distance += $distance_delta; - return $track_data; + if ( ! is_null( $point['time_offset'] ) && ! is_null( $prev_point['time_offset'] ) ) { + $time_delta = $point['time_offset'] - $prev_point['time_offset']; // seconds + if ( $time_delta > 0 ) { + $speed = ( $distance_delta * 3600 ) / $time_delta; // km/h + } + } + } + + $profiles['distance'][] = round( $cumulative_distance, 3 ); + $profiles['time'][] = $point['time_offset']; + $profiles['elevation'][] = ! is_null( $point['ele'] ) ? round( $point['ele'], 2 ) : null; + $profiles['speed'][] = ! is_null( $speed ) ? round( $speed, 1 ) : null; + $profiles['hr'][] = $point['hr']; + $profiles['cadence'][] = $point['cad']; + } + } + + return [ + 'points' => $map_points, + 'profiles' => $profiles, + ]; } function mystat_infographic_page() { @@ -1379,68 +1391,106 @@ function mystat_view_activity_page() { // Prepare map and chart data if GPX exists $gpx_data = []; + $has_gpx_data = false; if ( ! empty( $activity->gpx_url ) ) { $gpx_data = mystat_parse_gpx_data( $activity->gpx_url ); + $has_gpx_data = !empty($gpx_data['points']); + } - if ( ! empty( $gpx_data['points'] ) ) { - wp_enqueue_style('leaflet-css', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'); - wp_enqueue_script('leaflet-js', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', [], '1.9.4', true); - wp_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', [], null, true); - - wp_register_script('mystat-details-loader', false); - wp_enqueue_script('mystat-details-loader'); + if ($has_gpx_data) { + wp_enqueue_style('leaflet-css', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'); + wp_enqueue_script('leaflet-js', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', [], '1.9.4', true); + wp_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', [], null, true); + + wp_register_script('mystat-details-loader', false); + wp_enqueue_script('mystat-details-loader'); - $map_script = ' - const track_points = ' . json_encode($gpx_data['points']) . '; - if (typeof L !== "undefined" && track_points.length > 0) { - const map = L.map("mystat-activity-map"); - L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { - attribution: \'© OpenStreetMap contributors\' - }).addTo(map); - const polyline = L.polyline(track_points, {color: "' . esc_js( $activity->color ) . '"}).addTo(map); - map.fitBounds(polyline.getBounds().pad(0.1)); - L.marker(track_points[0]).addTo(map).bindPopup("Start"); - L.marker(track_points[track_points.length - 1]).addTo(map).bindPopup("Koniec"); - } - '; + // Check which profiles have data + $available_profiles = []; + if (!empty(array_filter($gpx_data['profiles']['elevation']))) $available_profiles['elevation'] = 'Wysokość'; + if (!empty(array_filter($gpx_data['profiles']['speed']))) $available_profiles['speed'] = 'Prędkość'; + if (!empty(array_filter($gpx_data['profiles']['hr']))) $available_profiles['hr'] = 'Tętno'; + if (!empty(array_filter($gpx_data['profiles']['cadence']))) $available_profiles['cadence'] = 'Kadencja'; + + $has_time_data = !empty(array_filter($gpx_data['profiles']['time'], fn($t) => !is_null($t))); - $elevation_chart_script = ''; - if ( ! empty( $gpx_data['elevation_profile'] ) ) { - $elevation_chart_script = ' - const elevation_data = ' . json_encode( $gpx_data['elevation_profile'] ) . '; - if (typeof Chart !== "undefined" && elevation_data.length > 0) { - const ctx = document.getElementById("mystat-elevation-chart").getContext("2d"); - new Chart(ctx, { - type: "line", - data: { - labels: elevation_data.map(p => p.distance), - datasets: [{ - label: "Wysokość (m)", - data: elevation_data.map(p => p.elevation), - borderColor: "' . esc_js( $activity->color ) . '", - backgroundColor: "' . esc_js( $activity->color ) . '20", - fill: true, - pointRadius: 0, - tension: 0.1 - }] - }, - options: { - responsive: true, maintainAspectRatio: false, - scales: { - x: { title: { display: true, text: "Dystans (km)" } }, - y: { title: { display: true, text: "Wysokość (m n.p.m.)" } } - }, - plugins: { legend: { display: false } } - } - }); - } - '; + $chart_js = ' + const track_points = ' . json_encode($gpx_data['points']) . '; + const profiles = ' . json_encode($gpx_data['profiles']) . '; + let activeChart = null; + + if (typeof L !== "undefined" && track_points.length > 0) { + const map = L.map("mystat-activity-map"); + L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: \'© OpenStreetMap\' }).addTo(map); + const polyline = L.polyline(track_points, {color: "' . esc_js( $activity->color ) . '"}).addTo(map); + map.fitBounds(polyline.getBounds().pad(0.1)); + L.marker(track_points[0]).addTo(map).bindPopup("Start"); + L.marker(track_points[track_points.length - 1]).addTo(map).bindPopup("Koniec"); } - wp_add_inline_script('mystat-details-loader', - 'document.addEventListener("DOMContentLoaded", function() {' . $map_script . $elevation_chart_script . '});' - ); - } + const chartConfigs = { + elevation: { label: "Wysokość", unit: "m n.p.m.", color: "#8e44ad" }, + speed: { label: "Prędkość", unit: "km/h", color: "#2980b9" }, + hr: { label: "Tętno", unit: "bpm", color: "#c0392b" }, + cadence: { label: "Kadencja", unit: "rpm", color: "#27ae60" } + }; + const xAxisConfigs = { + distance: { label: "Dystans (km)", data: profiles.distance }, + time: { + label: "Czas", data: profiles.time, + formatter: (s) => s === null ? "" : new Date(s * 1000).toISOString().substr(11, 8) + } + }; + + function renderChart() { + if (activeChart) activeChart.destroy(); + const chartType = document.querySelector(".mystat-chart-tabs .nav-tab-active").getAttribute("href").substring(1); + const xAxisType = document.querySelector(\'input[name="mystat_xaxis"]:checked\').value; + + const yData = profiles[chartType], xData = xAxisConfigs[xAxisType].data; + const filteredY = [], filteredX = []; + if(yData) { + for(let i=0; i v, maxRotation: 0, autoSkip: true, maxTicksLimit: 10 } }, + y: { title: { display: true, text: chartConfigs[chartType].unit } } + }, + plugins: { legend: { display: false } }, + interaction: { intersect: false, mode: "index" }, + } + }); + } + + document.querySelectorAll(".mystat-chart-tabs .nav-tab").forEach(t => t.addEventListener("click", e => { + e.preventDefault(); + document.querySelector(".mystat-chart-tabs .nav-tab-active").classList.remove("nav-tab-active"); + e.target.classList.add("nav-tab-active"); + renderChart(); + })); + document.querySelectorAll(\'input[name="mystat_xaxis"]\').forEach(r => r.addEventListener("change", renderChart)); + if (document.querySelector(".mystat-chart-tabs .nav-tab")) renderChart(); + '; + wp_add_inline_script('mystat-details-loader', 'document.addEventListener("DOMContentLoaded", function() {' . $chart_js . '});'); } ?> @@ -1497,24 +1547,44 @@ function mystat_view_activity_page() { Plik GPXPobierz plik GPX - - + +

Mapa Trasy

- -

Profil Wysokości

-
- + +

Wykresy

+
+
+ + +
+ Oś X:  + +   + +
+ + + +
+
+ +
gpx_url ) ) : ?>
-

Mapa Trasy

-

Nie udało się wczytać danych z pliku GPX lub plik jest uszkodzony/pusty.

+

Nie udało się wczytać danych z pliku GPX lub plik jest uszkodzony/pusty. Brak danych do wyświetlenia mapy i wykresów.

@@ -2142,84 +2212,129 @@ function mystat_single_activity_shortcode_handler( $atts ) { ob_start(); - // Prepare map data if GPX exists + // Prepare map and chart data if GPX exists $gpx_data = []; + $has_gpx_data = false; if ( ! empty( $activity->gpx_url ) ) { $gpx_data = mystat_parse_gpx_data( $activity->gpx_url ); + $has_gpx_data = !empty($gpx_data['points']); + } - if ( ! empty( $gpx_data['points'] ) ) { - // Enqueue scripts for the frontend - wp_enqueue_style('leaflet-css', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'); - wp_enqueue_script('leaflet-js', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', [], '1.9.4', true); - wp_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', [], null, true); - - $map_id = 'mystat-map-' . esc_attr( $activity->id ); - $chart_id = 'mystat-chart-' . esc_attr( $activity->id ); + $unique_id = 'mystat-activity-' . esc_attr( $activity->id ); - // Pass track data to a script - wp_register_script('mystat-shortcode-loader-' . $activity->id, false); - wp_enqueue_script('mystat-shortcode-loader-' . $activity->id); + if ($has_gpx_data) { + wp_enqueue_style('leaflet-css', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'); + wp_enqueue_script('leaflet-js', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', [], '1.9.4', true); + wp_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', [], null, true); + + $map_id = 'map-' . $unique_id; + $chart_id = 'chart-' . $unique_id; + + $available_profiles = []; + if (!empty(array_filter($gpx_data['profiles']['elevation']))) $available_profiles['elevation'] = 'Wysokość'; + if (!empty(array_filter($gpx_data['profiles']['speed']))) $available_profiles['speed'] = 'Prędkość'; + if (!empty(array_filter($gpx_data['profiles']['hr']))) $available_profiles['hr'] = 'Tętno'; + if (!empty(array_filter($gpx_data['profiles']['cadence']))) $available_profiles['cadence'] = 'Kadencja'; + + $has_time_data = !empty(array_filter($gpx_data['profiles']['time'], fn($t) => !is_null($t))); + + wp_register_script('mystat-shortcode-loader-' . $activity->id, false); + wp_enqueue_script('mystat-shortcode-loader-' . $activity->id); + + $js_script = ' + (function() { + document.addEventListener("DOMContentLoaded", function() { + const uniqueId = "' . esc_js($unique_id) . '"; + const containerEl = document.getElementById(uniqueId); + if (!containerEl || typeof L === "undefined" || typeof Chart === "undefined") return; - $map_script = ' const trackPoints = ' . json_encode( $gpx_data['points'] ) . '; + const profiles = ' . json_encode( $gpx_data['profiles'] ) . '; + let activeChart = null; + const mapId = "' . esc_js($map_id) . '"; - - var container = L.DomUtil.get(mapId); - if(container != null) { container._leaflet_id = null; } + const mapEl = document.getElementById(mapId); + if (mapEl && trackPoints.length > 0) { + if (mapEl._leaflet_id) mapEl._leaflet_id = null; + const map = L.map(mapId); + L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: \'© OpenStreetMap\' }).addTo(map); + const polyline = L.polyline(trackPoints, {color: "' . esc_js( $activity->category_color ) . '"}).addTo(map); + map.fitBounds(polyline.getBounds().pad(0.1)); + } - const map = L.map(mapId); - L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { - attribution: \'© OpenStreetMap\' - }).addTo(map); - - const polyline = L.polyline(trackPoints, {color: "' . esc_js( $activity->category_color ) . '"}).addTo(map); - map.fitBounds(polyline.getBounds().pad(0.1)); - '; + const chartId = "' . esc_js($chart_id) . '"; + const chartEl = document.getElementById(chartId); + if (!chartEl) return; - $elevation_chart_script = ''; - if ( ! empty( $gpx_data['elevation_profile'] ) ) { - $elevation_chart_script = ' - const elevationData = ' . json_encode( $gpx_data['elevation_profile'] ) . '; - const chartId = "' . esc_js($chart_id) . '"; - const ctx = document.getElementById(chartId).getContext("2d"); - new Chart(ctx, { + const chartConfigs = { + elevation: { label: "Wysokość", unit: "m n.p.m.", color: "#8e44ad" }, + speed: { label: "Prędkość", unit: "km/h", color: "#2980b9" }, + hr: { label: "Tętno", unit: "bpm", color: "#c0392b" }, + cadence: { label: "Kadencja", unit: "rpm", color: "#27ae60" } + }; + const xAxisConfigs = { + distance: { label: "Dystans (km)", data: profiles.distance }, + time: { label: "Czas", data: profiles.time, formatter: (s) => s === null ? "" : new Date(s * 1000).toISOString().substr(11, 8) } + }; + + function renderChart() { + if (activeChart) activeChart.destroy(); + const activeTab = containerEl.querySelector(".mystat-chart-tab.active"); + if (!activeTab) return; + const chartType = activeTab.dataset.type; + const xAxisRadio = containerEl.querySelector(\'input[name="xaxis-\' + uniqueId + \'"]:checked\'); + if (!xAxisRadio) return; + const xAxisType = xAxisRadio.value; + + const yData = profiles[chartType], xData = xAxisConfigs[xAxisType].data; + const filteredY = [], filteredX = []; + if(yData) { + for(let i=0; i p.distance), + labels: filteredX, datasets: [{ - label: "Wysokość (m)", - data: elevationData.map(p => p.elevation), - borderColor: "' . esc_js( $activity->category_color ) . '", - backgroundColor: "' . esc_js( $activity->category_color ) . '20", + label: chartConfigs[chartType].label, data: filteredY, + borderColor: chartConfigs[chartType].color, backgroundColor: chartConfigs[chartType].color + "20", fill: true, pointRadius: 0, tension: 0.1 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { - x: { title: { display: true, text: "Dystans (km)" } }, - y: { title: { display: true, text: "Wysokość (m n.p.m.)" } } + x: { title: { display: true, text: xAxisConfigs[xAxisType].label }, ticks: { callback: xAxisType === "time" ? xAxisConfigs.time.formatter : (v) => v, maxRotation: 0, autoSkip: true, maxTicksLimit: 7 } }, + y: { title: { display: true, text: chartConfigs[chartType].unit } } }, - plugins: { legend: { display: false } } + plugins: { legend: { display: false } }, + interaction: { intersect: false, mode: "index" }, } }); - '; - } + } - wp_add_inline_script('mystat-shortcode-loader-' . $activity->id, - '(function() { - document.addEventListener("DOMContentLoaded", function() { - if (typeof L === "undefined" || typeof Chart === "undefined") return; - ' . $map_script . ' - ' . $elevation_chart_script . ' - }); - })();' - ); - } + containerEl.querySelectorAll(".mystat-chart-tab").forEach(t => t.addEventListener("click", e => { + e.preventDefault(); + containerEl.querySelector(".mystat-chart-tab.active").classList.remove("active"); + e.currentTarget.classList.add("active"); + renderChart(); + })); + containerEl.querySelectorAll(\'input[name="xaxis-\' + uniqueId + \'"]\').forEach(r => r.addEventListener("change", renderChart)); + if (containerEl.querySelector(".mystat-chart-tab")) renderChart(); + }); + })();'; + wp_add_inline_script('mystat-shortcode-loader-' . $activity->id, $js_script); } ?> -
+

title ); ?>

date ) ) ); ?>

@@ -2247,14 +2362,35 @@ function mystat_single_activity_shortcode_handler( $atts ) {
- -
- + +
- -
- -
+ +
+
+
+ $label ) : ?> + + +
+ +
+ Oś X:  + +   + +
+ + + +
+
+ +
+
+