Public
Edited
Jun 20, 2023
Insert cell
Insert cell
cassettes = new Map([
[11,12,13,14,15,16,17,19,21,23,25],
[11,12,13,14,15,16,17,19,21,23,26],
[11,12,13,14,15,16,17,19,22,25,28],
[11,12,13,14,15,17,19,21,24,27,30],
[11,12,13,14,15,17,19,22,25,28,32],
].map(cassette => [`${cassette[0]}/${cassette[cassette.length - 1]}T`, cassette]))
Insert cell
chainrings = new Map([
[38],
[40],
[42],
[44],
[46],
[48],
[50],
[52],
[50, 34],
[52, 36],
[53, 39],
].map(chainring => [chainring.join('/') + 'T', chainring]))
Insert cell
Insert cell
Plot.dot(Array.from(cassettes.entries()).map(([size, cassette]) => cassette.map(sprocket => ({
sprocket,
size
}))).flat(), {
x: 'sprocket',
y: 'size',
fill: 'red',
}).plot({
x: {label: null, interval: 1},
y: {label: null},
})
Insert cell
Insert cell
Plot.plot({
padding: 0,
grid: true,
x: {axis: "top", label: "Chainring"},
y: {label: "Cassette"},
color: {type: "linear", scheme: "PiYG"},
marks: [
Plot.cell(gearSets, {x: "chainring", y: "cassette", fill: "ratio", inset: 0.5}),
Plot.text(gearSets, {x: "chainring", y: "cassette", text: d => d.ratio?.toFixed(2), fill: "black"})
]
})
Insert cell
viewof cadence = Inputs.range([60, 120], {label: "Cadence", step: 1})
Insert cell
viewof wheelSize = Inputs.select(wheelSizes, {label: "Wheel Size"})
Insert cell
viewof cruisingSpeed = Inputs.range([0, 100], {label: "Cruising Speed", step: 0.1})
Insert cell
Insert cell
Plot.plot({
padding: 0,
grid: true,
x: {axis: "top", label: "Chainring"},
y: {label: "Cassette"},
color: {type: "linear", scheme: "PiYG"},
marks: [
Plot.cell(gearSets, {x: "chainring", y: "cassette", fill: "ratio", inset: 0.5}),
Plot.text(gearSets, {x: "chainring", y: "cassette", text: d => ratioToSpeed(d.ratio).toFixed(2), fill: "black"})
]
})
Insert cell
Insert cell
viewof cassette = Inputs.select(cassettes, {label: "Cassette"})
Insert cell
Plot.dot(collectChainringRatio(cassette), {
x: 'chainring',
y: 'ratio',
fill: 'red',
title: d => `${d.chainringGear} - ${d.cassetteGear} in ${d.speed}km/h`
}).plot({
x: {type: 'point'},
marks: [
Plot.ruleY([speedToRatio(cruisingSpeed)]),
],
})
Insert cell
viewof chainring = Inputs.select(chainrings, {label: "Chainring"})
Insert cell
Plot.dot(collectCassetteRatio(chainring), {
x: 'cassette',
y: 'ratio',
fill: 'red',
title: d => `${d.chainringGear} - ${d.cassetteGear} in ${d.speed}km/h`
}).plot({
x: {type: 'point'},
marks: [
Plot.ruleY([speedToRatio(cruisingSpeed)]),
],
})
Insert cell
Insert cell
viewof compareForm = Inputs.form({
chainringL: Inputs.select(chainrings, {label: "Chainring"}),
cassetteL: Inputs.select(cassettes, {label: "Cassette"}),
chainringR: Inputs.select(chainrings, {label: "Chainring"}),
cassetteR: Inputs.select(cassettes, {label: "Cassette"}),
})
Insert cell
Plot.plot({
style: "overflow: visible;",
y: {label: null},
x: {grid: true, interval: 1},
marks: [
Plot.ruleY([speedToRatio(cruisingSpeed)]),
Plot.lineY(compareSets(compareForm), {x: "gear", y: "ratio", stroke: "set"}),
Plot.dotY(compareSets(compareForm), {x: "gear", y: "ratio", fill: "red", title: d => `${d.chainringGear} - ${d.cassetteGear} in ${d.speed}km/h`}),
Plot.text(compareSets(compareForm), Plot.selectLast({x: "gear", y: "ratio", z: "set", text: "set", textAnchor: "start", dx: 3}))
]
})
Insert cell
Insert cell
gearSets = collectValueInMap(chainrings).map(chainring => collectValueInMap(cassettes).map(cassette => ({
chainring,
cassette,
ratio: chainring / cassette,
}))).flat()
Insert cell
wheelSizes = new Map([
['700 x 19c', 2080],
['700 x 20c', 2086],
['700 x 23c', 2096],
['700 x 25c', 2105],
['700 x 28c', 2136],
['700 x 30c', 2170],
['700 x 32c', 2155],
['700 x 35c', 2168],
['700 x 38c', 2180],
['700 x 40c', 2200],
['700 Tubular', 2130],
])
Insert cell
collectValueInMap = m => Array.from(new Set(Array.from(m.entries()).reduce((r, c) => r.concat(c[1]), []))).sort()
Insert cell
collectChainringRatio = cassette => Array.from(chainrings.entries()).map(([chainring, chainringGears]) => {
const r = []
traverseSet(cassette, chainringGears, (cassetteGear, chainringGear) => {
const ratio = chainringGear / cassetteGear
r.push({
chainring,
cassetteGear,
chainringGear,
ratio,
speed: ratioToSpeed(ratio).toFixed(2),
})
})

return r
}).flat()
Insert cell
collectCassetteRatio = chainringGears => Array.from(cassettes.entries()).map(([cassette, cassetteGears]) => {
const r = []
traverseSet(cassetteGears, chainringGears, (cassetteGear, chainringGear) => {
const ratio = chainringGear / cassetteGear
r.push({
cassette,
cassetteGear,
chainringGear,
ratio,
speed: ratioToSpeed(ratio).toFixed(2),
})
})

return r
}).flat()
Insert cell
ratioToSpeed = ratio => ratio * cadence * wheelSize * 60 / 1000000
Insert cell
speedToRatio = speed => speed / cadence / wheelSize / 60 * 1000000
Insert cell
compareSets = formInput => {
const r = []
const collectSet = (cassette, chainring) => {
let i = 1
traverseSet(cassette, chainring, (cassetteGear, chainringGear) => {
const ratio = chainringGear / cassetteGear
r.push({
set: `${chainring.join('/')}T - ${cassette[0]}/${cassette[cassette.length - 1]}T`,
ratio,
speed: ratioToSpeed(ratio).toFixed(2),
chainringGear,
cassetteGear,
gear: i++,
})
})
}

collectSet(formInput.cassetteL, formInput.chainringL)
collectSet(formInput.cassetteR, formInput.chainringR)

return r
}
Insert cell
traverseSet = (cassetteGears, chainringGears, f) => {
for (const cassetteGear of cassetteGears) {
for (const chainringGear of chainringGears) {
// 非单盘系统去掉大盘大飞、小盘小飞,这种状态下链条不稳定且尺比重合
if (chainringGears.length > 1) {
if (cassetteGear === Math.min(...chainringGears) && chainringGear === Math.min(...chainringGears)) continue
if (cassetteGear === Math.max(...chainringGears) && chainringGear === Math.max(...chainringGears)) continue
}

f(cassetteGear, chainringGear)
}
}
}
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