123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218 |
- // Vector Math
- const vectorDot = (u, v) => u.map((x, i) => x * v[i]).reduce((x, y) => x + y);
- const vectorMag = (v) => Math.sqrt(vectorDot(v, v));
- // Angle Math
- const rad2deg = 180 / Math.PI;
- // Misc
- const clamp = (mn, v, mx) => Math.min(Math.max(v, mn), mx);
- // Contrast
- const getContrastingTextColor = (hex) => {
- const { r, g, b } = d3.color(hex);
- return vectorDot([r, g, b], [0.3, 0.6, 0.1]) >= 128
- ? "var(--color-dark)"
- : "var(--color-light)";
- };
- // "Visual Importance"
- const calcImportance = (chroma, lightness, proportion) =>
- chroma +
- Math.tanh(100 * (chroma - 0.25)) + // penalty for being <25%
- Math.tanh(100 * (chroma - 0.4)) + // penalty for being <40%
- lightness +
- Math.tanh(100 * (lightness - 0.5)) + // penalty for being <50%
- proportion +
- Math.tanh(100 * (proportion - 0.05)) + // penalty for being <5%
- Math.tanh(100 * (proportion - 0.1)) + // penalty for being <15%
- Math.tanh(100 * (proportion - 0.15)) + // penalty for being <15%
- Math.tanh(100 * (proportion - 0.25)) + // penalty for being <25%
- Math.tanh(100 * (proportion - 0.8)); // penalty for being <50%
- // Conversions
- const jab2hex = (jab) => d3.jab(...jab).formatHex();
- const rgb2hex = (rgb) => d3.rgb(...rgb).formatHex();
- const jab2hue = (jab) => d3.jch(d3.jab(...jab)).h || 0;
- const rgb2hue = (rgb) => d3.hsl(d3.rgb(...rgb)).h || 0;
- const jab2lit = ([j]) => j / 100;
- const rgb2lit = (rgb) => d3.hsl(d3.rgb(...rgb)).l || 0;
- const jab2chroma = (jab) => d3.jch(d3.jab(...jab)).C / 100;
- const rgb2chroma = (rgb) => d3.jch(d3.rgb(...rgb)).C / 100;
- // Pre-computation
- const buildVectorData = (vector, toHue, toLightness, toChroma, toHex) => {
- const sqMag = vectorDot(vector, vector);
- const mag = Math.sqrt(sqMag);
- const unit = vector.map((c) => c / mag);
- const hue = toHue(vector);
- const lightness = toLightness(vector);
- const chroma = toChroma(vector);
- const hex = toHex(vector);
- return { vector, sqMag, mag, unit, hue, lightness, chroma, hex };
- };
- const buildClusterData = (
- size,
- inertia,
- mu1,
- mu2,
- mu3,
- nu1,
- nu2,
- nu3,
- totalSize,
- toHue,
- toLightness,
- toChroma,
- toHex
- ) => {
- const mu = buildVectorData([mu1, mu2, mu3], toHue, toLightness, toChroma, toHex);
- const nu = [nu1, nu2, nu3];
- const muNuAngle = rad2deg * Math.acos(vectorDot(mu.unit, nu) / vectorMag(nu));
- const proportion = size / totalSize;
- const importance = calcImportance(mu.chroma, mu.lightness, proportion);
- return {
- size,
- inverseSize: 1 / size,
- inertia,
- mu,
- nu,
- muNuAngle,
- proportion,
- inverseProportion: 1 / proportion,
- importance,
- };
- };
- const buildPokemonData = ([name, size, ...values]) => ({
- name,
- jab: {
- total: buildClusterData(
- size,
- ...values.slice(0, 7),
- size,
- jab2hue,
- jab2lit,
- jab2chroma,
- jab2hex
- ),
- clusters: [
- buildClusterData(
- ...values.slice(7, 15),
- size,
- jab2hue,
- jab2lit,
- jab2chroma,
- jab2hex
- ),
- buildClusterData(
- ...values.slice(15, 23),
- size,
- jab2hue,
- jab2lit,
- jab2chroma,
- jab2hex
- ),
- buildClusterData(
- ...values.slice(23, 31),
- size,
- jab2hue,
- jab2lit,
- jab2chroma,
- jab2hex
- ),
- buildClusterData(
- ...values.slice(31, 39),
- size,
- jab2hue,
- jab2lit,
- jab2chroma,
- jab2hex
- ),
- ].filter((c) => c.size !== 0),
- },
- rgb: {
- total: buildClusterData(
- size,
- ...values.slice(39, 46),
- size,
- rgb2hue,
- rgb2lit,
- rgb2chroma,
- rgb2hex
- ),
- clusters: [
- buildClusterData(
- ...values.slice(46, 54),
- size,
- rgb2hue,
- rgb2lit,
- rgb2chroma,
- rgb2hex
- ),
- buildClusterData(
- ...values.slice(54, 62),
- size,
- rgb2hue,
- rgb2lit,
- rgb2chroma,
- rgb2hex
- ),
- buildClusterData(
- ...values.slice(62, 70),
- size,
- rgb2hue,
- rgb2lit,
- rgb2chroma,
- rgb2hex
- ),
- buildClusterData(
- ...values.slice(70, 78),
- size,
- rgb2hue,
- rgb2lit,
- rgb2chroma,
- rgb2hex
- ),
- ].filter((c) => c.size !== 0),
- },
- });
- const pokemonData = databaseV3.map((row) => buildPokemonData(row));
- const calcScores = (data, target) => {
- const sigma = Math.sqrt(
- data.inertia - 2 * vectorDot(data.mu.vector, target.vector) + target.sqMag
- );
- const bigTheta = 1 - vectorDot(data.nu, target.unit);
- const rawPhi = Math.abs(data.mu.hue - target.hue);
- return {
- sigma,
- bigTheta,
- alpha: sigma * Math.pow(bigTheta, target.chroma + target.lightness),
- theta: rad2deg * Math.acos(vectorDot(data.mu.unit, target.unit)),
- phi: Math.min(rawPhi, 360 - rawPhi),
- delta: vectorMag(data.mu.vector.map((x, i) => x - target.vector[i])),
- manhattan: data.mu.vector
- .map((x, i) => Math.abs(x - target.vector[i]))
- .reduce((x, y) => x + y),
- ch: Math.max(...data.mu.vector.map((x, i) => Math.abs(x - target.vector[i]))),
- lightnessDiff: Math.abs(data.mu.lightness - target.lightness),
- inertia: data.inertia,
- variance: data.inertia - data.mu.sqMag,
- muNuAngle: data.muNuAngle,
- size: data.size,
- lightness: data.mu.lightness,
- chroma: data.mu.chroma,
- importance: data.importance,
- inverseSize: data.inverseSize,
- proportion: data.proportion,
- inverseProportion: data.inverseProportion,
- muHex: data.mu.hex,
- };
- };
|