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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more