Public
Edited
Jun 12, 2024
Importers
2 stars
Insert cell
md`# Pokemon Dataviz`
Insert cell
Insert cell
Insert cell
Insert cell
numSpecies = _(pokedex).map(speciesName).uniq().value().length
Insert cell
function getEvolutionStage(pokemon) {
if (!pokemon.prevo) {
return 0
}
return getEvolutionStage(pokemonByName.get(pokemon.prevo)) + 1
}
Insert cell
function getEvolutionChain(pokemon) {
if (!pokemon) return []
return getEvolutionChain(pokemonByName.get(pokemon.prevo)).concat([pokemon])
}
Insert cell
pokemonTypeList = _(pokemonByType).map((list, type) => ({ type, list })).sortBy(entry => entry.type).value()
Insert cell
function sortTypes(types) {
return _.sortBy(types)
}
Insert cell
evolutionFamilies = baseFormStageMons.map(mon => getEvolutionFamily(mon.name))
Insert cell
function getEvolutionFamily(monName) {
const family = []
const stack = [monName]
while (stack.length > 0) {
const current = stack.pop()
if (!family.includes(current)) {
family.push(current)
}
for (let relative of relatedMons(current)) {
if (!family.includes(relative)) {
stack.push(relative)
}
}
}
return family
}
Insert cell
function relatedMons(monName) {
const mon = pokemonByName.get(monName)
const related = getAllFormes(monName)
if (mon.prevo) {
related.push(mon.prevo)
}
if (mon.evos) {
related.push(...mon.evos)
}
if (mon.canGigantamax) {
related.push(`${monName}-Gmax`)
}
return related
}
Insert cell
baseFormStageMons = pokedexList.filter(mon => !mon.prevo && !mon.baseSpecies)
Insert cell
function getAllFormes(pokemonName) {
const pokemon = pokemonByName.get(pokemonName)
if (!pokemon) {
throw new Error("Cannot find pokemon " + pokemonName)
}
const base = !!pokemon.baseSpecies ? pokemonByName.get(pokemon.baseSpecies) : pokemon
return [base.name, ...(base.otherFormes ?? [])]
}
Insert cell
pokemonWithEvolutionChain = _.flatMap(evolutionChains.filter(c => c.length > 1), chain => chain.map(stage => ({...stage, chain: chain[chain.length - 1].name})))
Insert cell
// TODO deduplicate different forms with the same typing
_(pokedexList)
.filter(({ types }) => types.length > 1)
.groupBy(({ types }) => sortTypes(types))
.toPairs()
.map(([typeCombo, pokeList]) => ({ typeCombo, pokeList: _.uniqBy(pokeList, speciesName).map(entry => entry.name) }))
.sortBy(({typeCombo, pokeList}) => -pokeList.length)
.value()
Insert cell
pokedex.bulbasaur
Insert cell
md`## Pokemon that change color when evolving`
Insert cell
colorChanges = pokedexList.filter(pokemon => pokemon.prevo && pokemonByName.get(pokemon.prevo).color !== pokemon.color).map(pokemon => pokemon.name);
Insert cell
md`${colorChanges.join(', ')}`
Insert cell
Insert cell
// pokemon with biggest ev to base total ratio
_.sortBy(
pokedexList.filter((mon) => !!mon.evYield),
(mon) => baseStatTotal(mon) / evYieldTotal(mon)
).map((mon) => mon.name)
Insert cell
function evYieldTotal(mon) {
return _.sum(Object.values(mon.evYield || {}))
}
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
_.groupBy(pokedexList, mon => mon.growthRate)
Insert cell
pokedexList.filter(mon => mon.growthRate === 'Slow' && baseStatTotal(mon) === 600 && !isMega(mon) && !isLegendary(mon) && !isMythical(mon))
Insert cell
growthRates = [
"Slow",
"Medium Slow",
"Medium Fast",
"Fast",
"Fluctuating",
"Erratic"
]
Insert cell
Insert cell
{
const typePokedex = pokedexList.filter((mon) => {
if (mon.baseSpecies) {
return !_.isEqual(mon.types, getPokemon(mon.baseSpecies).types);
}
return true;
});
const counts = Object.fromEntries(
types.map((type) => [
type,
Object.fromEntries(_.range(1, 10).map((i) => [i, 0]))
])
);
for (const mon of typePokedex) {
for (const type of mon.types) {
counts[type][mon.gen]++;
}
}
const sums = [];
for (const type of types) {
for (const gen of _.range(1, 10)) {
sums.push({
type,
gen,
// count: _.sum(_.range(1, gen + 1).map((i) => counts[type][i]))
count: counts[type][gen]
});
}
}
return vl
.markArea()
.data(sums)
.width(600)
.height(400)
.encode(
vl.x().field("gen"),
vl.y().fieldQ("count").aggregate("sum"),
vl.tooltip().fieldQ("count"),
vl
.color()
.fieldN("type")
.scale({
domain: types,
range: types.map((t) => typeColors[t])
})
)
.render();
}
Insert cell
Insert cell
_.mapValues(pokemonByType, (mons, type) => {
return mons.filter((mon) => {
return mon.prevo && !pokemonByName.get(mon.prevo).types.includes(type)
}).map(mon => mon.name);
})
Insert cell
_.mapValues(pokemonByType, (mons, type) => {
return mons.filter((mon) => {
return mon.evos && _.some(mon.evos, evoName => !pokemonByName.get(evoName).types.includes(type))
}).map(mon => mon.name)
})
Insert cell
// TODO this might ignore Pokemon with visual forms with different colors
typeColorCount = {
const dedupedList = pokedexList.filter((mon) => {
if (mon.baseSpecies) {
return !_.isEqual(mon.types, getPokemon(mon.baseSpecies).types)
}
if (["Arceus", "Silvally"].includes(mon.baseSpecies)) {
return false
}
return true
});
const tally = [];
for (const type of types) {
for (const color of colors) {
tally.push({
type,
color,
count: dedupedList.filter(
(mon) => mon.types.includes(type) && mon.color === color
).length
});
}
}
return tally;
}
Insert cell
colors = [
"Black",
"Blue",
"Brown",
"Gray",
"Green",
"Pink",
"Purple",
"Red",
"White",
"Yellow"
]
Insert cell
vl.markCircle()
.title("Types and colors")
.data(typeColorCount)
.encode(
vl.x().field("type"),
vl.y().field("color"),
vl.size().fieldQ("count"),
vl.tooltip().field("count")
)
.render()
Insert cell
// TODO do this by evolution family instead (note: watch out for baby mons)
vl.markCircle()
.title("types and egg groups")
.data(pokemonWithTypeEgg.map(x => ({ ...x, count: 1})))
.encode(
vl.x().fieldO("type"),
vl.y().fieldO("eggGroup"),
vl.size().sum("count"),
)
.render()
Insert cell
pokemonWithTypeEgg = _.flatMap(pokemonWithType, mon => mon.eggGroups.map(eggGroup => ({ ...mon, eggGroup })))
Insert cell
// TODO do this by evolution family instead? wait... some of them have gender-based evolutions
vl.markCircle()
.title("Types and Gender")
.data(pokemonWithTypeGender.filter(p => !p.eggGroups.includes("Undiscovered") && !isSameTypeForme(p)).map(x => ({ ...x, count: 1})))
.encode(
vl.x().fieldO("type"),
vl.y().fieldO("genderRate"),
vl.size().sum("count"),
)
.render()
Insert cell
pokemonWithTypeGender = _.map(pokemonWithType, pokemon => ({ ...pokemon, genderRate: displayGenderRatio(getGenderCode(pokemon))}))
Insert cell
// unused egg group combinations
{
const unused = []
for (const eggGroup1 of eggGroups.slice(0, eggGroups.length - 2)) {
for (const eggGroup2 of eggGroups.slice(eggGroups.indexOf(eggGroup1), eggGroups.length - 2)) {
if (!pokedexList.some(mon => mon.eggGroups.includes(eggGroup1) && mon.eggGroups.includes(eggGroup2))) {
unused.push([eggGroup1, eggGroup2])
}
}
}
return unused
}
Insert cell
function speciesName(pokemon) {
return pokemon.baseSpecies || pokemon.name
}
Insert cell
function gcd(a, b) {
while (b > 0) {
let t = b
b = a % b
a = t
}
return a
}
Insert cell
// get an integer such that M / 8 is the ratio of males
function getGenderCode(pokemon) {
if (pokemon.genderRatio) {
return pokemon.genderRatio.M * 8
}
if (pokemon.gender) {
switch (pokemon.gender) {
case 'F': return 0
case 'M': return 8
case 'N': return -1
}
}
return 4
}
Insert cell
function displayGenderRatio(code) {
if (code === -1) {
return "Genderless";
}
if (code === 8) {
return "♂ Only";
}
if (code === 0) {
return "♀ Only";
}
const m = code;
const f = 8 - code;
const factor = gcd(m, f);
return `${m / factor}♂ : ${f / factor}♀`;
}
Insert cell
babyPokemon = pokedexList.filter(isBaby)
Insert cell
function isBaby(pokemon) {
return pokemon.eggGroups.includes("Undiscovered") && pokemon.canHatch
}
Insert cell
function isMega(pokemon) {
return pokemon.forme && pokemon.forme.includes("Mega")
}
Insert cell
function isGmax(pokemon) {
if (!pokemon.forme) return false;
return pokemon.forme.includes("Gmax") || pokemon.forme.includes("Eternamax");
}
Insert cell
function isTotem(pokemon) {
return pokemon.name.includes("-Totem");
}
Insert cell
// Formes that pokemon take in battle that do not change type
battleFormes = ["Cherrim", "Mimikyu", "Cramorant", "Eiscue", "Morpeko", "Xerneas", "Wishiwashi"]
Insert cell
genderFormes = ["Meowstic", "Indeedee"]
Insert cell
// formes only available in certain events/games
eventFormes = ["Pikachu", "Pichu", "Eevee", "Greninja", "Vivillon", "Floette", "Magearna"]
Insert cell
function isSameStatsForme(pokemon) {
if (!pokemon.baseSpecies) return false
const base = pokemonByName.get(pokemon.baseSpecies)
return _.isEqual(pokemon.baseStats, base.baseStats)
}
Insert cell
pokedexList.filter(mon => mon.baseSpecies && !isMega(mon) && !isRegionalForme(mon) && !isSameStatsForme(mon))
Insert cell
// Return whether this pokemon is an alternate forme with the same height and weight as its base
function isSameDimsForme(pokemon) {
// Don't count totem Pokemon
if (isTotem(pokemon)) return true
if (!pokemon.baseSpecies) return false
// Regional variants, Gmax, and mega always counted separately
if (isGmax(pokemon) || isMega(pokemon) || isRegionalForme(pokemon)) return false
const base = pokemonByName.get(pokemon.baseSpecies)
return pokemon.heightm === base.heightm && pokemon.weightkg === base.weightkg
}
Insert cell
pokedexList.filter(mon => mon.baseSpecies && !isMega(mon) && !isGmax(mon) && !isRegionalForme(mon) && !isSameDimsForme(mon))
Insert cell
// Return whether this pokemon is an alternate form with the same type as its base
function isSameTypeForme(pokemon, options = {}) {
// Ignore Arceus and Silvally alts since they can be *any* type
const { arceus = false } = options;
if (isTotem(pokemon) || isGmax(pokemon)) return true;
// Arceus and Silvally forms shouldn't count, since there is one of each
if (!arceus && ["Arceus", "Silvally"].includes(pokemon.baseSpecies))
return true;
return (
pokemon.baseSpecies &&
_.isEqual(pokemon.types, pokemonByName.get(pokemon.baseSpecies).types)
);
}
Insert cell
// List all pokemon that don't fulfill this criterion
pokedexList.filter(mon => mon.baseSpecies && !isGmax(mon) && !isMega(mon) && !isRegionalForme(mon) && !isSameTypeForme(mon))
Insert cell
function isRegionalForme(pokemon) {
if (!pokemon.forme) return false
if (pokemon.baseSpecies === "Pikachu") return false
return pokemon.forme.includes("Galar") || pokemon.forme.includes("Alola") || pokemon.forme.includes("Hisui") || pokemon.forme.includes("Paldea")
}
Insert cell
function isFormWithSprite(pokemon) {
if (isTotem(pokemon)) return false
if (pokemon.forme && pokemon.forme.includes("Starter")) return false
if (pokemon.forme && pokemon.forme.includes("Spiky-eared")) return false
if (["Eternal", "Antique", "Starter", "Spiky-eared"].includes(pokemon.forme)) return false
if (pokemon.baseSpecies === "Pikachu" && ["Cosplay", "Rock-Star", "Belle", "Pop-Star", "PhD", "Libre", "World", "Partner"].includes(pokemon.forme)) return false
return true
}
Insert cell
evolutionChains = pokedexList
.filter(mon => (!mon.baseSpecies || isRegionalForme(mon)) && !isTotem(mon))
.filter(isFinalEvolution)
.map(pokemon => getEvolutionChain(pokemon).map(stage => ({ ...stage, stage: getEvolutionStage(stage) } )))
Insert cell
function isFinalEvolution(pokemon) {
// TODO factor our all these "extras" to another function
return !pokemon.evos && !isMega(pokemon) && !isGmax(pokemon)
}
Insert cell
types = ["Normal", "Fighting", "Flying", "Poison", "Ground", "Rock", "Bug", "Ghost", "Steel", "Fire", "Water", "Grass", "Electric", "Psychic", "Ice", "Dragon", "Dark", "Fairy"]
Insert cell
typesToIndex = _.mapValues(_.invert(types), val => parseInt(val))
Insert cell
pokedex = FileAttachment("pokedex@7.json").json()
Insert cell
pokemonByName = new Map(pokedexList.map(entry => [entry.name, entry]))
Insert cell
pokemonByType = _.groupBy(pokemonWithType, 'type')
Insert cell
// Pokemon with entries duplicated for each type that the Pokemon has
pokemonWithType = _(pokedexList)
.flatMap(pokemon => pokemon.types.map(type => ({ ...pokemon, type })))
.value()
Insert cell
function getClass(mon) {
if (isMega(mon)) return "Mega";
if (isGmax(mon)) return "Gmax";
if (isUltraBeast(mon)) return "Ultra Beast";
if (isParadox(mon)) return "Paradox";
if (isBaby(mon)) return "Baby";
if (isStarter(mon)) return "Starter";
if (isFossil(mon)) return "Fossil";
if (isMythical(mon)) return "Mythical";
if (isLegendary(mon)) return "Legendary";
return "Regular"
}
Insert cell
function isFossil(mon) { return fossilPokemon.includes(mon.name)}
Insert cell
pokedexList = Object.values(pokedex)
Insert cell
function isUltraBeast(mon) {
return mon.abilities[0] === 'Beast Boost'
}
Insert cell
function isParadox(mon) {
return ["Protosynthesis", "Quark Drive"].includes(mon.abilities[0])
}
Insert cell
function isStarter(mon) {
return ["Overgrow", "Blaze", "Torrent"].includes(mon.abilities[0])
}
Insert cell
Insert cell
function isLegendary(mon) {
if (!mon.eggGroups.includes("Undiscovered")) return false;
if (
[
"Nidorina",
"Nidoqueen",
"Unown",
"Gimmighoul",
"Gimmighoul-Roaming",
"Gholdengo"
].includes(mon.name)
)
return false;
if (
[
"Eevee",
"Pikachu",
"Pichu",
"Floette",
"Greninja",
"Sinistea",
"Polteageist"
].includes(mon.baseSpecies)
)
return false;
if (["Dracovish", "Dracozolt", "Arctovish", "Arctozolt"].includes(mon.name))
return false;
if (isMythical(mon) || isParadox(mon) || isUltraBeast(mon) || isBaby(mon))
return false;
return true;
}
Insert cell
function isSubLegendary(mon) {
return (
isLegendary(mon) || isMythical(mon) || isParadox(mon) || isUltraBeast(mon)
);
}
Insert cell
function isMythical(mon) {
return mythicalPokemon.includes(mon.name) || mythicalPokemon.includes(mon.baseSpecies)
}
Insert cell
mythicalPokemon = ["Mew", "Celebi", "Jirachi", "Deoxys", "Shaymin", "Shaymin-Sky", "Manaphy", "Phione", "Darkrai", "Arceus", "Victini", "Keldeo", "Meloetta", "Genesect", "Diancie", "Hoopa", "Volcanion", "Magearna", "Marshadow", "Zeraora", "Meltan", "Melmetal", "Zarude"]

Insert cell
function printExtremes(title, n, list) {
return md`## ${title}

## Top ${n}

${_.takeRight(list, n).reverse().map(x => `1. ${x}`).join('\n')}

## Bottom ${n}

${_.take(list, n).map(x => `1. ${x}`).join('\n')}
`
}
Insert cell
function normalizeName(name) {
return name
.toLowerCase()
.replace(/[.:’'%]/g, "")
.replace(/é/g, "e")
.replace(" ", "-")
}
Insert cell
function formatGen(number) {
const gens = ['0', 'I', "II", 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X']
return gens[number]
}
Insert cell
function getPokemon(name) {
if (!pokemonByName.has(name)) {
throw new Error(`Cannot find Pokemon ${name}`)
}
return pokemonByName.get(name)
}
Insert cell
pokedexList.filter(mon => mon.cosmeticFormes)
Insert cell
_ = require('lodash')
Insert cell
d3 = require("d3@5")
Insert cell
function getPrevo(mon) {
return pokemonByName.get(mon.prevo)
}
Insert cell
function baseStatTotal(mon) {
return _.sum(Object.values(mon.baseStats))
}
Insert cell
eggGroups = [
"Amorphous",
"Bug",
"Dragon",
"Fairy",
"Field",
"Flying",
"Grass",
"Human-Like",
"Mineral",
"Monster",
"Water 1",
"Water 2",
"Water 3",
"Undiscovered",
"Ditto"
]
Insert cell
typeColors = ({
Normal: '#A8A77A',
Fire: '#EE8130',
Water: '#6390F0',
Electric: '#F7D02C',
Grass: '#7AC74C',
Ice: '#96D9D6',
Fighting: '#C22E28',
Poison: '#A33EA1',
Ground: '#E2BF65',
Flying: '#A98FF3',
Psychic: '#F95587',
Bug: '#A6B91A',
Rock: '#B6A136',
Ghost: '#735797',
Dragon: '#6F35FC',
Dark: '#705746',
Steel: '#B7B7CE',
Fairy: '#D685AD',
});
Insert cell
function getSpriteName(pokemon) {
const species = normalizeName(pokemon.baseSpecies || pokemon.name);

if (["Zacian", "Zamazenta"].includes(pokemon.name)) {
return `${species}-hero`;
}
if (pokemon.forme && pokemon.forme === "F") {
return `${species}-female`;
}
if (pokemon.name === "Vivillon-Pokeball") {
return "vivillon-poke-ball";
}

if (isRegionalForme(pokemon)) {
if (pokemon.baseSpecies === "Darmanitan" && pokemon.forme === "Galar") {
return "darmanitan-galarian-standard";
}
return normalizeName(pokemon.name)
.replace("alola", "alolan")
.replace("galar", "galarian")
.replace("hisui", "hisuian")
.replace("paldea", "paldean");
}
if (pokemon.name === "Eternatus-Eternamax") {
return normalizeName(pokemon.name);
}
if (isGmax(pokemon)) {
return `${species}-gigantamax`;
}
if (pokemon.name.endsWith("-Starter")) {
return `${pokemon.baseSpecies.toLowerCase()}-lets-go`;
}
if (pokemon.baseSpecies === "Pikachu") {
return `${normalizeName(pokemon.name)}-cap`;
}
if (pokemon.name === "Maushold-Four") {
return `maushold-family4`;
}
if (pokemon.name === 'Minior') {
return 'minior-core';
}
if (pokemon.baseSpecies === "Calyrex") {
return `calyrex-${pokemon.forme.toLowerCase()}-rider`;
}
return normalizeName(pokemon.name);
}
Insert cell
function getSpriteUrl(pokemon) {
const path = pokemon.num > 898 || pokemon.name.includes('-Hisui') || pokemon.name.includes('-Paldea') || ['Dialga-Origin', 'Palkia-Origin', 'Basculin-White-Striped'].includes(pokemon.name) ? 'scarlet-violet' : 'sword-shield'
const category = isGmax(pokemon) ? 'normal' : 'icon'
return `https://img.pokemondb.net/sprites/${path}/${category}/${getSpriteName(pokemon)}.png`
}
Insert cell
import { vl } from "@vega/vega-lite-api-v5"
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