More charts

This commit is contained in:
2026-02-02 14:51:21 +01:00
parent ce03ed5f64
commit 71bcc75498
2 changed files with 368 additions and 211 deletions
+21
View File
@@ -37,3 +37,24 @@
flex: 1 1 48%; /* Pozwala na elastyczne dopasowanie, z bazową szerokością 48% */ flex: 1 1 48%; /* Pozwala na elastyczne dopasowanie, z bazową szerokością 48% */
min-width: 300px; /* Zapobiega zbytniemu ściskaniu kolumn na mniejszych ekranach */ 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;
}
+347 -211
View File
@@ -774,113 +774,125 @@ function mystat_yearly_summary_page() {
* @return array An array containing 'points' for the map and 'elevation_profile'. * @return array An array containing 'points' for the map and 'elevation_profile'.
*/ */
function mystat_parse_gpx_data( $gpx_url ) { function mystat_parse_gpx_data( $gpx_url ) {
if ( empty( $gpx_url ) ) { if ( empty( $gpx_url ) ) {
return []; 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 ) { if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
return []; return [];
} }
$gpx_content = wp_remote_retrieve_body( $response ); $gpx_content = wp_remote_retrieve_body( $response );
if ( empty( $gpx_content ) ) { if ( empty( $gpx_content ) ) {
return []; return [];
} }
// --- Privacy Zone --- // --- Privacy Zone ---
$privacy_options = get_option( 'mystat_privacy_options' ); $privacy_options = get_option( 'mystat_privacy_options' );
$privacy_enabled = false; $privacy_enabled = ! empty( $privacy_options['latitude'] ) && ! empty( $privacy_options['longitude'] ) && ! empty( $privacy_options['radius'] );
$privacy_center_lat = 0; $privacy_center_lat = $privacy_enabled ? (float) $privacy_options['latitude'] : 0;
$privacy_center_lon = 0; $privacy_center_lon = $privacy_enabled ? (float) $privacy_options['longitude'] : 0;
$privacy_radius_km = 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'] ) ) { libxml_use_internal_errors( true );
$privacy_enabled = true; $gpx = simplexml_load_string( $gpx_content );
$privacy_center_lat = (float) $privacy_options['latitude']; libxml_clear_errors();
$privacy_center_lon = (float) $privacy_options['longitude'];
$privacy_radius_km = ( (int) $privacy_options['radius'] ) / 1000; // Convert meters to km
}
libxml_use_internal_errors( true ); if ( $gpx === false ) {
$gpx = simplexml_load_string( $gpx_content ); return [];
libxml_clear_errors(); }
if ( $gpx === false ) { // Use XPath to be more robust against different GPX structures/namespaces
return []; $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 = [ $raw_points = [];
'points' => [], $start_time = null;
'elevation_profile' => [],
];
$raw_points = [];
// Extract raw points with lat, lon, ele foreach ( $trackpoints as $trkpt ) {
if ( isset( $gpx->trk ) ) { if ( isset( $trkpt['lat'], $trkpt['lon'] ) ) {
foreach ( $gpx->trk->trkseg as $trkseg ) { $extensions = $trkpt->extensions ? $trkpt->extensions->children( 'gpxtpx', true ) : null;
foreach ( $trkseg->trkpt as $trkpt ) { $time = isset( $trkpt->time ) ? strtotime( (string) $trkpt->time ) : null;
if ( isset( $trkpt['lat'], $trkpt['lon'] ) ) { if ( $time && is_null( $start_time ) ) {
$raw_points[] = [ $start_time = $time;
'lat' => (float) $trkpt['lat'], }
'lon' => (float) $trkpt['lon'],
'ele' => isset( $trkpt->ele ) ? (float) $trkpt->ele : null,
];
}
}
}
}
if ( empty( $raw_points ) ) { $hr_val = ( $extensions && isset( $extensions->TrackPointExtension->hr ) ) ? (int) $extensions->TrackPointExtension->hr : null;
return $track_data; $cad_val = ( $extensions && isset( $extensions->TrackPointExtension->cad ) ) ? (int) $extensions->TrackPointExtension->cad : null;
}
// Process raw points to calculate profile $raw_points[] = [
$cumulative_distance = 0; 'lat' => (float) $trkpt['lat'],
$elevation_profile = []; 'lon' => (float) $trkpt['lon'],
$map_points = []; '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 ) { if ( empty( $raw_points ) ) {
$earth_radius = 6371; // in km return [];
$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;
};
foreach ( $raw_points as $i => $point ) { // Process raw points to calculate profiles
// Check if point is inside privacy zone $map_points = [];
$is_in_privacy_zone = false; $profiles = [ 'distance' => [], 'time' => [], 'elevation' => [], 'speed' => [], 'hr' => [], 'cadence' => [] ];
if ( $privacy_enabled ) { $cumulative_distance = 0;
$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;
}
}
// Only process points outside the privacy zone $haversine = function( $lat1, $lon1, $lat2, $lon2 ) {
if ( ! $is_in_privacy_zone ) { $earth_radius = 6371; // in km
$map_points[] = [ $point['lat'], $point['lon'] ]; $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 ) { foreach ( $raw_points as $i => $point ) {
$prev_point = $raw_points[ $i - 1 ]; $is_in_privacy_zone = false;
$cumulative_distance += $haversine( $prev_point['lat'], $prev_point['lon'], $point['lat'], $point['lon'] ); 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'] ) ) { if ( ! $is_in_privacy_zone ) {
$elevation_profile[] = [ 'distance' => round( $cumulative_distance, 3 ), 'elevation' => round( $point['ele'], 2 ) ]; $map_points[] = [ $point['lat'], $point['lon'] ];
} $speed = null;
}
}
$track_data['points'] = $map_points; if ( $i > 0 ) {
$track_data['elevation_profile'] = $elevation_profile; $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() { function mystat_infographic_page() {
@@ -1379,68 +1391,106 @@ function mystat_view_activity_page() {
// Prepare map and chart data if GPX exists // Prepare map and chart data if GPX exists
$gpx_data = []; $gpx_data = [];
$has_gpx_data = false;
if ( ! empty( $activity->gpx_url ) ) { if ( ! empty( $activity->gpx_url ) ) {
$gpx_data = mystat_parse_gpx_data( $activity->gpx_url ); $gpx_data = mystat_parse_gpx_data( $activity->gpx_url );
$has_gpx_data = !empty($gpx_data['points']);
}
if ( ! empty( $gpx_data['points'] ) ) { if ($has_gpx_data) {
wp_enqueue_style('leaflet-css', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'); 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('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_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', [], null, true);
wp_register_script('mystat-details-loader', false); wp_register_script('mystat-details-loader', false);
wp_enqueue_script('mystat-details-loader'); wp_enqueue_script('mystat-details-loader');
$map_script = ' // Check which profiles have data
const track_points = ' . json_encode($gpx_data['points']) . '; $available_profiles = [];
if (typeof L !== "undefined" && track_points.length > 0) { if (!empty(array_filter($gpx_data['profiles']['elevation']))) $available_profiles['elevation'] = 'Wysokość';
const map = L.map("mystat-activity-map"); if (!empty(array_filter($gpx_data['profiles']['speed']))) $available_profiles['speed'] = 'Prędkość';
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { if (!empty(array_filter($gpx_data['profiles']['hr']))) $available_profiles['hr'] = 'Tętno';
attribution: \'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors\' if (!empty(array_filter($gpx_data['profiles']['cadence']))) $available_profiles['cadence'] = 'Kadencja';
}).addTo(map);
const polyline = L.polyline(track_points, {color: "' . esc_js( $activity->color ) . '"}).addTo(map); $has_time_data = !empty(array_filter($gpx_data['profiles']['time'], fn($t) => !is_null($t)));
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");
}
';
$elevation_chart_script = ''; $chart_js = '
if ( ! empty( $gpx_data['elevation_profile'] ) ) { const track_points = ' . json_encode($gpx_data['points']) . ';
$elevation_chart_script = ' const profiles = ' . json_encode($gpx_data['profiles']) . ';
const elevation_data = ' . json_encode( $gpx_data['elevation_profile'] ) . '; let activeChart = null;
if (typeof Chart !== "undefined" && elevation_data.length > 0) {
const ctx = document.getElementById("mystat-elevation-chart").getContext("2d"); if (typeof L !== "undefined" && track_points.length > 0) {
new Chart(ctx, { const map = L.map("mystat-activity-map");
type: "line", L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: \'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>\' }).addTo(map);
data: { const polyline = L.polyline(track_points, {color: "' . esc_js( $activity->color ) . '"}).addTo(map);
labels: elevation_data.map(p => p.distance), map.fitBounds(polyline.getBounds().pad(0.1));
datasets: [{ L.marker(track_points[0]).addTo(map).bindPopup("Start");
label: "Wysokość (m)", L.marker(track_points[track_points.length - 1]).addTo(map).bindPopup("Koniec");
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 } }
}
});
}
';
} }
wp_add_inline_script('mystat-details-loader', const chartConfigs = {
'document.addEventListener("DOMContentLoaded", function() {' . $map_script . $elevation_chart_script . '});' 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<yData.length; i++) {
if (yData[i] !== null) {
filteredY.push(yData[i]);
filteredX.push(xData[i]);
}
}
}
const ctx = document.getElementById("mystat-details-chart").getContext("2d");
activeChart = new Chart(ctx, {
type: "line",
data: {
labels: filteredX,
datasets: [{
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: xAxisConfigs[xAxisType].label }, ticks: { callback: xAxisType === "time" ? xAxisConfigs.time.formatter : (v) => 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() {
<tr><th scope="row">Plik GPX</th><td><a href="<?php echo esc_url( $activity->gpx_url ); ?>" target="_blank">Pobierz plik GPX</a></td></tr> <tr><th scope="row">Plik GPX</th><td><a href="<?php echo esc_url( $activity->gpx_url ); ?>" target="_blank">Pobierz plik GPX</a></td></tr>
<?php endif; ?> <?php endif; ?>
</table> </table>
<?php if ( ! empty( $gpx_data['points'] ) ) : ?> <?php if ( $has_gpx_data ) : ?>
<hr> <hr>
<h3>Mapa Trasy</h3> <h3>Mapa Trasy</h3>
<div id="mystat-activity-map" style="height: 450px; width: 100%; border: 1px solid #ddd; margin-bottom: 20px;"></div> <div id="mystat-activity-map" style="height: 450px; width: 100%; border: 1px solid #ddd; margin-bottom: 20px;"></div>
<?php if ( ! empty( $gpx_data['elevation_profile'] ) ) : ?> <?php if ( ! empty( $available_profiles ) ) : ?>
<h3>Profil Wysokości</h3> <h3>Wykresy</h3>
<div style="position: relative; height:250px; width:100%;"> <div class="mystat-charts-container">
<canvas id="mystat-elevation-chart"></canvas> <div class="mystat-chart-controls">
<nav class="nav-tab-wrapper mystat-chart-tabs">
<?php
$is_first = true;
foreach ( $available_profiles as $key => $label ) : ?>
<a href="#<?php echo esc_attr($key); ?>" class="nav-tab <?php if ($is_first) { echo 'nav-tab-active'; $is_first = false; } ?>"><?php echo esc_html($label); ?></a>
<?php endforeach; ?>
</nav>
<?php if ($has_time_data): ?>
<div class="mystat-xaxis-switcher">
<strong>Oś X:</strong>&nbsp;
<label><input type="radio" name="mystat_xaxis" value="distance" checked> Dystans</label>
&nbsp;
<label><input type="radio" name="mystat_xaxis" value="time"> Czas</label>
</div>
<?php else: ?>
<input type="hidden" name="mystat_xaxis" value="distance" checked>
<?php endif; ?>
</div>
<div style="position: relative; height:250px; width:100%;">
<canvas id="mystat-details-chart"></canvas>
</div>
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php elseif ( ! empty( $activity->gpx_url ) ) : ?> <?php elseif ( ! empty( $activity->gpx_url ) ) : ?>
<hr> <hr>
<h3>Mapa Trasy</h3>
<div class="notice notice-warning inline"> <div class="notice notice-warning inline">
<p>Nie udało się wczytać danych z pliku GPX lub plik jest uszkodzony/pusty.</p> <p>Nie udało się wczytać danych z pliku GPX lub plik jest uszkodzony/pusty. Brak danych do wyświetlenia mapy i wykresów.</p>
</div> </div>
<?php endif; ?> <?php endif; ?>
@@ -2142,84 +2212,129 @@ function mystat_single_activity_shortcode_handler( $atts ) {
ob_start(); ob_start();
// Prepare map data if GPX exists // Prepare map and chart data if GPX exists
$gpx_data = []; $gpx_data = [];
$has_gpx_data = false;
if ( ! empty( $activity->gpx_url ) ) { if ( ! empty( $activity->gpx_url ) ) {
$gpx_data = mystat_parse_gpx_data( $activity->gpx_url ); $gpx_data = mystat_parse_gpx_data( $activity->gpx_url );
$has_gpx_data = !empty($gpx_data['points']);
}
if ( ! empty( $gpx_data['points'] ) ) { $unique_id = 'mystat-activity-' . esc_attr( $activity->id );
// 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 );
// Pass track data to a script if ($has_gpx_data) {
wp_register_script('mystat-shortcode-loader-' . $activity->id, false); wp_enqueue_style('leaflet-css', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css');
wp_enqueue_script('mystat-shortcode-loader-' . $activity->id); 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 trackPoints = ' . json_encode( $gpx_data['points'] ) . ';
const profiles = ' . json_encode( $gpx_data['profiles'] ) . ';
let activeChart = null;
const mapId = "' . esc_js($map_id) . '"; const mapId = "' . esc_js($map_id) . '";
const mapEl = document.getElementById(mapId);
var container = L.DomUtil.get(mapId); if (mapEl && trackPoints.length > 0) {
if(container != null) { container._leaflet_id = null; } 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: \'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>\' }).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); const chartId = "' . esc_js($chart_id) . '";
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { const chartEl = document.getElementById(chartId);
attribution: \'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>\' if (!chartEl) return;
}).addTo(map);
const polyline = L.polyline(trackPoints, {color: "' . esc_js( $activity->category_color ) . '"}).addTo(map);
map.fitBounds(polyline.getBounds().pad(0.1));
';
$elevation_chart_script = ''; const chartConfigs = {
if ( ! empty( $gpx_data['elevation_profile'] ) ) { elevation: { label: "Wysokość", unit: "m n.p.m.", color: "#8e44ad" },
$elevation_chart_script = ' speed: { label: "Prędkość", unit: "km/h", color: "#2980b9" },
const elevationData = ' . json_encode( $gpx_data['elevation_profile'] ) . '; hr: { label: "Tętno", unit: "bpm", color: "#c0392b" },
const chartId = "' . esc_js($chart_id) . '"; cadence: { label: "Kadencja", unit: "rpm", color: "#27ae60" }
const ctx = document.getElementById(chartId).getContext("2d"); };
new Chart(ctx, { 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<yData.length; i++) {
if (yData[i] !== null) {
filteredY.push(yData[i]);
filteredX.push(xData[i]);
}
}
}
const ctx = chartEl.getContext("2d");
activeChart = new Chart(ctx, {
type: "line", type: "line",
data: { data: {
labels: elevationData.map(p => p.distance), labels: filteredX,
datasets: [{ datasets: [{
label: "Wysokość (m)", label: chartConfigs[chartType].label, data: filteredY,
data: elevationData.map(p => p.elevation), borderColor: chartConfigs[chartType].color, backgroundColor: chartConfigs[chartType].color + "20",
borderColor: "' . esc_js( $activity->category_color ) . '",
backgroundColor: "' . esc_js( $activity->category_color ) . '20",
fill: true, pointRadius: 0, tension: 0.1 fill: true, pointRadius: 0, tension: 0.1
}] }]
}, },
options: { options: {
responsive: true, maintainAspectRatio: false, responsive: true, maintainAspectRatio: false,
scales: { scales: {
x: { title: { display: true, text: "Dystans (km)" } }, 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: "Wysokość (m n.p.m.)" } } 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, containerEl.querySelectorAll(".mystat-chart-tab").forEach(t => t.addEventListener("click", e => {
'(function() { e.preventDefault();
document.addEventListener("DOMContentLoaded", function() { containerEl.querySelector(".mystat-chart-tab.active").classList.remove("active");
if (typeof L === "undefined" || typeof Chart === "undefined") return; e.currentTarget.classList.add("active");
' . $map_script . ' renderChart();
' . $elevation_chart_script . ' }));
}); 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);
} }
?> ?>
<div class="mystat-single-activity-shortcode"> <div class="mystat-single-activity-shortcode" id="<?php echo esc_attr($unique_id); ?>">
<h4><?php echo esc_html( $activity->title ); ?></h4> <h4><?php echo esc_html( $activity->title ); ?></h4>
<p><em><?php echo esc_html( date_i18n( 'j F Y', strtotime( $activity->date ) ) ); ?></em></p> <p><em><?php echo esc_html( date_i18n( 'j F Y', strtotime( $activity->date ) ) ); ?></em></p>
@@ -2247,14 +2362,35 @@ function mystat_single_activity_shortcode_handler( $atts ) {
</div> </div>
</div> </div>
<?php if ( ! empty( $gpx_data['points'] ) ) : ?> <?php if ( $has_gpx_data ) : ?>
<div id="<?php echo $map_id; ?>" style="height: 350px; width: 100%; margin-top: 15px; border-radius: 5px; margin-bottom: 20px;"></div> <div id="<?php echo esc_attr($map_id); ?>" class="mystat-single-map" style="height: 350px; width: 100%; margin-top: 15px; border-radius: 5px; margin-bottom: 20px;"></div>
<?php endif; ?>
<?php if ( ! empty( $gpx_data['elevation_profile'] ) ) : ?> <?php if ( ! empty( $available_profiles ) ) : ?>
<div style="position: relative; height:250px; width:100%;"> <div class="mystat-charts-container">
<canvas id="<?php echo $chart_id; ?>"></canvas> <div class="mystat-chart-controls" style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 10px; gap: 10px;">
</div> <div class="mystat-chart-tabs" style="display: flex; flex-wrap: wrap; gap: 5px;">
<?php
$is_first = true;
foreach ( $available_profiles as $key => $label ) : ?>
<button data-type="<?php echo esc_attr($key); ?>" class="mystat-chart-tab <?php if ($is_first) { echo 'active'; $is_first = false; } ?>"><?php echo esc_html($label); ?></button>
<?php endforeach; ?>
</div>
<?php if ($has_time_data): ?>
<div class="mystat-xaxis-switcher" style="padding-top: 5px; font-size: 0.9em; white-space: nowrap;">
<strong>Oś X:</strong>&nbsp;
<label><input type="radio" name="xaxis-<?php echo esc_attr($unique_id); ?>" value="distance" checked> Dystans</label>
&nbsp;
<label><input type="radio" name="xaxis-<?php echo esc_attr($unique_id); ?>" value="time"> Czas</label>
</div>
<?php else: ?>
<input type="hidden" name="xaxis-<?php echo esc_attr($unique_id); ?>" value="distance" checked>
<?php endif; ?>
</div>
<div class="mystat-chart-wrapper" style="position: relative; height:250px; width:100%;">
<canvas id="<?php echo esc_attr($chart_id); ?>"></canvas>
</div>
</div>
<?php endif; ?>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php <?php