Public
Edited
Dec 20
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
crossStorage = new CrossStorageClient("https://strava-atlas.herokuapp.com/hub.html")
Insert cell
selectedActDataJson = crossStorageListen(crossStorage, 'selectedActData')
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
tracks = unfilteredTracks.filter(d => {
if (d["@attr"] && d["@attr"].nowplaying) {
return false;
}
return true;
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
stravaUrl = `https://corsproxy.io/?https://www.strava.com/api/v3/activities/${actData.id}/streams/latlng,time,altitude?resolution=${resolution}`
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
lngFactor = Math.cos((latRange[0] + latRange[1]) / 2 * Math.PI / 180)

// From now on, 'lng' means 'lng scaled by lngFactor'
Insert cell
Insert cell
Insert cell
indexInterpolated = (array, value) => {
const i = _.sortedIndex(array, value);
if (i === 0) { return 0; }
if (i === array.length) { return array.length - 1; }
return i - 1 + (value - array[i - 1]) / (array[i] - array[i - 1]);
}
Insert cell
elementInterpolated = (array, i) => {
if (i <= 0) { return array[0]; }
if (i >= array.length - 1) { return array[array.length - 1]; }
const int = Math.floor(i);
const frac = i % 1;
return array[int] * (1 - frac) + array[int + 1] * frac;
}
Insert cell
secsData = _.find(streams, {type: 'time'}).data
Insert cell
Insert cell
curIdx = indexInterpolated(secsData, secs)
Insert cell
{
const margin = 200;
const marginRight = margin;
const [canvasWidth, canvasHeight] = [width, 700];
const [insetWidth, insetHeight] = [canvasWidth - margin - marginRight, canvasHeight - margin * 2];
const scale = Math.min(insetWidth / (lngRange[1] - lngRange[0]),
insetHeight / (latRange[1] - latRange[0]));
const centerLng = (lngRange[0] + lngRange[1]) / 2;
const centerLat = (latRange[0] + latRange[1]) / 2;
const centerX = margin + insetWidth / 2;
const centerY = canvasHeight / 2;
const lngToX = (lng) => (lng - centerLng) * scale + centerX;
const latToY = (lat) => -(lat - centerLat) * scale + centerY;

const ctx = DOM.context2d(canvasWidth, canvasHeight);
ctx.beginPath();
latlngs.forEach((latlng, i) => {
const [x, y] = [lngToX(latlng[1] * lngFactor), latToY(latlng[0])];
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.stroke();
const curLat = elementInterpolated(lats, curIdx);
const curLng = elementInterpolated(lngs, curIdx);
ctx.fillStyle = 'blue';
ctx.beginPath();
ctx.arc(lngToX(curLng), latToY(curLat), 3, 0, Math.PI * 2, true);
ctx.fill();

const ringRadius = insetHeight * 0.5
// ctx.beginPath();
// ctx.arc(centerX, centerY, ringRadius, 0, Math.PI * 2, true);
// ctx.stroke();

const actStartSecs = +(new Date(actData.start_date)) / 1000;
tracks.forEach(track => {
const offsetSecs = track.date.uts - actStartSecs;
const idx = indexInterpolated(secsData, offsetSecs);
const curLat = elementInterpolated(lats, idx);
const curLng = elementInterpolated(lngs, idx);
const [x, y] = [lngToX(curLng), latToY(curLat)];
const n = ringRadius / Math.hypot(x - centerX, y - centerY);
const [xN, yN] = [(x - centerX) * n + centerX, (y - centerY) * n + centerY];
ctx.strokeStyle = '#f00';
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(xN, yN);
ctx.stroke();
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.arc(x, y, 3, 0, Math.PI * 2, true);
ctx.fill();
ctx.fillStyle = 'gray';
ctx.font = '12px Arial';
const msg = `${track.artist["#text"]} - ${track.name}`;
ctx.textAlign = xN < centerX ? "end" : "start";
const offset = xN < centerX ? -5 : 5;
ctx.fillText(msg, xN + offset, yN + 6);
});
return ctx.canvas;
}
Insert cell
{
const track = tracks[0];
const trackT = +track.date.uts;
const actT = +(new Date(actData.start_date)) / 1000;
const secs = track.date.uts - actT;
return {trackT, actT, secs};
}
Insert cell
distance = streams[1].data.map(d => d / 1609)
Insert cell
altitude = streams[2].data.map(a => a * 3.281)
Insert cell
altitudeDiffs = altitude.map((a, i) => i === 0 ? 0 : a - altitude[i - 1])
Insert cell
altitudeCum = {
let ret = [];
let cum = 0;
altitudeDiffs.forEach((d, i) => {
cum += Math.max(d, 0);
ret.push(cum);
});
return ret;
}
Insert cell
altitudeBreakLabels = {
let ret = [];
let last = 0;
altitudeCum.forEach((d, i) => {
if (d % 1000 < last % 1000) {
ret.push({n: Math.floor(d / 1000), x: distance[i]});
}
last = d
});
return ret;
}
Insert cell
// given an increasing array ys and an interval,
// return (interpolated) indexes where ys crosses interval * n for integer n.

function yBreaks(ys, interval) {
let lastY = undefined;
let nextBreak = undefined;
let result = [];
ys.forEach((y, i) => {
console.log(y, i, lastY, nextBreak);
if (lastY === undefined) {
nextBreak = Math.ceil(y / interval) * interval;
// TODO: hack
if (nextBreak === 0) {
nextBreak += interval;
}
} else {
while (nextBreak < y) {
result.push(i - 1 + (nextBreak - lastY) / (y - lastY));
nextBreak += interval;
}
}
lastY = y;
});
return result;
}
Insert cell
altitudeBreakIndices = yBreaks(altitudeCum, 250)
Insert cell
altitudeBreakDots = altitudeBreakIndices.map((i) => ({
distance: elementInterpolated(distance, i),
altitude: elementInterpolated(altitude, i),
}))
Insert cell
yBreaks([0, 1.5, 3, 4], 1);
Insert cell
indexInterpolated
Insert cell
df = _.map(
_.zip(distance, altitude, altitudeCum),
([distance, altitude, altitudeCum]) => ({
distance,
altitude,
altitudeCum,
altitudeCum100: (altitudeCum) % 500,
altitudeGrp: Math.floor(altitudeCum / 500)
})
)
Insert cell
htl.html`
${Plot.plot({
marks: [
Plot.lineY(df, {x: "distance", y: "altitude"}),
// Plot.dot(altitudeBreakDots, {x: "distance", y: "altitude", symbol: "plus", r: 4}),
Plot.text(altitudeBreakDots, {x: "distance", y: "altitude", text: () => "|", fontSize: 15, dy: -1, rotate: -0}),
],
x: {grid: true, label: null, tickFormat: () => ""},
y: {label: null},
height: 150,
})}

<div style="margin-top: -25px" />
${Plot.plot({
marks: [
Plot.lineY(df, {x: "distance", y: "altitudeCum"}),
],
x: {grid: true, label: null, tickFormat: () => ""},
y: {
// grid: true,
label: null,
// ticks: [_.last(altitudeCum) / 2], tickFormat: (d, i) => "1/2"
},
height: 240
})}

<div style="margin-top: -20px" />
${Plot.plot({
marks: [
Plot.lineY(df, {x: "distance", y: "altitudeCum100", z: "altitudeGrp"}),
Plot.text(altitudeBreakLabels, {x: "x", y: 0, text: (d) => d.n + 'K', dx: 0, dy: -12, lineAnchor: "bottom"}),
],
x: {grid: true, label: null},
y: {grid: true, label: null, ticks: [250]},
height: 80
})}
`
Insert cell
Insert cell
Insert cell
_ = require("lodash")
Insert cell
import { CrossStorageClient, crossStorageListen } from '@joshuahhh/cross-storage';
Insert cell
import {slider, select} from "@jashkenas/inputs"
Insert cell
Insert cell
streams = {
stravaUrl = `https://corsproxy.io/?https://www.strava.com/api/v3/activities/${actData.id}/streams/latlng,time,altitude?resolution=${resolution}`
const headers = new Headers();
headers.append("Authorization", `Bearer ${token.access_token}`);
const resp = await fetch(stravaUrl, {headers})
// console.log(await resp.text())
return resp.json();
}


https://www.strava.com/api/v3/athlete/activities?per_page=${per_page}&page=${page}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more