math.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. // Vector Math
  2. const vectorDot = (u, v) => u.map((x, i) => x * v[i]).reduce((x, y) => x + y);
  3. const vectorMag = (v) => Math.sqrt(vectorDot(v, v));
  4. // Angle Math
  5. const rad2deg = 180 / Math.PI;
  6. // Misc
  7. const clamp = (mn, v, mx) => Math.min(Math.max(v, mn), mx);
  8. const productLift =
  9. (...factors) =>
  10. (...args) =>
  11. factors
  12. .filter((fn) => !!fn)
  13. .map((fn) => fn(...args))
  14. .reduce((x, y) => x * y, 1);
  15. const mapValues = (obj, fn) =>
  16. Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, fn(value, key)]));
  17. // Contrast
  18. const getContrastingTextColor = (hex) => {
  19. const { r, g, b } = d3.color(hex);
  20. return vectorDot([r, g, b], [0.3, 0.6, 0.1]) >= 128
  21. ? "var(--color-dark)"
  22. : "var(--color-light)";
  23. };
  24. // "Visual Importance"
  25. const calcImportance = (chroma, lightness, proportion) =>
  26. chroma +
  27. Math.tanh(100 * (chroma - 0.25)) + // penalty for being <25%
  28. Math.tanh(100 * (chroma - 0.4)) + // penalty for being <40%
  29. lightness +
  30. Math.tanh(100 * (lightness - 0.5)) + // penalty for being <50%
  31. proportion +
  32. Math.tanh(100 * (proportion - 0.05)) + // penalty for being <5%
  33. Math.tanh(100 * (proportion - 0.1)) + // penalty for being <15%
  34. Math.tanh(100 * (proportion - 0.15)) + // penalty for being <15%
  35. Math.tanh(100 * (proportion - 0.25)) + // penalty for being <25%
  36. Math.tanh(100 * (proportion - 0.8)); // penalty for being <50%
  37. // Conversions
  38. const jab2hex = (jab) => d3.jab(...jab).formatHex();
  39. const rgb2hex = (rgb) => d3.rgb(...rgb).formatHex();
  40. const jab2hue = (jab) => d3.jch(d3.jab(...jab)).h || 0;
  41. const rgb2hue = (rgb) => d3.hsl(d3.rgb(...rgb)).h || 0;
  42. const jab2lit = ([j]) => j / 100;
  43. const rgb2lit = (rgb) => d3.hsl(d3.rgb(...rgb)).l || 0;
  44. const jab2chroma = (jab) => d3.jch(d3.jab(...jab)).C / 100;
  45. const rgb2chroma = (rgb) => d3.jch(d3.rgb(...rgb)).C / 100;
  46. // Pre-computation
  47. const buildVectorData = (vector, toHue, toLightness, toChroma, toHex) => {
  48. const sqMag = vectorDot(vector, vector);
  49. const mag = Math.sqrt(sqMag);
  50. const unit = vector.map((c) => c / mag);
  51. const hue = toHue(vector);
  52. const lightness = toLightness(vector);
  53. const chroma = toChroma(vector);
  54. const hex = toHex(vector);
  55. return { vector, sqMag, mag, unit, hue, lightness, chroma, hex };
  56. };
  57. const buildClusterData = (
  58. size,
  59. inertia,
  60. mu1,
  61. mu2,
  62. mu3,
  63. nu1,
  64. nu2,
  65. nu3,
  66. totalSize,
  67. toHue,
  68. toLightness,
  69. toChroma,
  70. toHex
  71. ) => {
  72. const mu = buildVectorData([mu1, mu2, mu3], toHue, toLightness, toChroma, toHex);
  73. const nu = [nu1, nu2, nu3];
  74. const muNuAngle = rad2deg * Math.acos(vectorDot(mu.unit, nu) / vectorMag(nu));
  75. const proportion = size / totalSize;
  76. const importance = calcImportance(mu.chroma, mu.lightness, proportion);
  77. return {
  78. size,
  79. inverseSize: 1 / size,
  80. inertia,
  81. mu,
  82. nu,
  83. muNuAngle,
  84. proportion,
  85. inverseProportion: 1 / proportion,
  86. importance,
  87. };
  88. };
  89. const buildPokemonData = ([name, size, ...values]) => ({
  90. name,
  91. jab: {
  92. total: buildClusterData(
  93. size,
  94. ...values.slice(0, 7),
  95. size,
  96. jab2hue,
  97. jab2lit,
  98. jab2chroma,
  99. jab2hex
  100. ),
  101. clusters: [
  102. buildClusterData(
  103. ...values.slice(7, 15),
  104. size,
  105. jab2hue,
  106. jab2lit,
  107. jab2chroma,
  108. jab2hex
  109. ),
  110. buildClusterData(
  111. ...values.slice(15, 23),
  112. size,
  113. jab2hue,
  114. jab2lit,
  115. jab2chroma,
  116. jab2hex
  117. ),
  118. buildClusterData(
  119. ...values.slice(23, 31),
  120. size,
  121. jab2hue,
  122. jab2lit,
  123. jab2chroma,
  124. jab2hex
  125. ),
  126. buildClusterData(
  127. ...values.slice(31, 39),
  128. size,
  129. jab2hue,
  130. jab2lit,
  131. jab2chroma,
  132. jab2hex
  133. ),
  134. ].filter((c) => c.size !== 0),
  135. },
  136. rgb: {
  137. total: buildClusterData(
  138. size,
  139. ...values.slice(39, 46),
  140. size,
  141. rgb2hue,
  142. rgb2lit,
  143. rgb2chroma,
  144. rgb2hex
  145. ),
  146. clusters: [
  147. buildClusterData(
  148. ...values.slice(46, 54),
  149. size,
  150. rgb2hue,
  151. rgb2lit,
  152. rgb2chroma,
  153. rgb2hex
  154. ),
  155. buildClusterData(
  156. ...values.slice(54, 62),
  157. size,
  158. rgb2hue,
  159. rgb2lit,
  160. rgb2chroma,
  161. rgb2hex
  162. ),
  163. buildClusterData(
  164. ...values.slice(62, 70),
  165. size,
  166. rgb2hue,
  167. rgb2lit,
  168. rgb2chroma,
  169. rgb2hex
  170. ),
  171. buildClusterData(
  172. ...values.slice(70, 78),
  173. size,
  174. rgb2hue,
  175. rgb2lit,
  176. rgb2chroma,
  177. rgb2hex
  178. ),
  179. ].filter((c) => c.size !== 0),
  180. },
  181. });
  182. const pokemonData = databaseV3.map((row) => buildPokemonData(row));
  183. const calcScores = (data, target) => {
  184. const sigma = Math.sqrt(
  185. data.inertia - 2 * vectorDot(data.mu.vector, target.vector) + target.sqMag
  186. );
  187. const bigTheta = 1 - vectorDot(data.nu, target.unit);
  188. const rawPhi = Math.abs(data.mu.hue - target.hue);
  189. return {
  190. sigma,
  191. bigTheta,
  192. alpha: sigma * Math.pow(bigTheta, target.chroma + target.lightness),
  193. theta: rad2deg * Math.acos(vectorDot(data.mu.unit, target.unit)),
  194. phi: Math.min(rawPhi, 360 - rawPhi),
  195. delta: vectorMag(data.mu.vector.map((x, i) => x - target.vector[i])),
  196. manhattan: data.mu.vector
  197. .map((x, i) => Math.abs(x - target.vector[i]))
  198. .reduce((x, y) => x + y),
  199. ch: Math.max(...data.mu.vector.map((x, i) => Math.abs(x - target.vector[i]))),
  200. lightnessDiff: Math.abs(data.mu.lightness - target.lightness),
  201. inertia: data.inertia,
  202. variance: data.inertia - data.mu.sqMag,
  203. muNuAngle: data.muNuAngle,
  204. size: data.size,
  205. lightness: data.mu.lightness,
  206. chroma: data.mu.chroma,
  207. importance: data.importance,
  208. inverseSize: data.inverseSize,
  209. proportion: data.proportion,
  210. inverseProportion: data.inverseProportion,
  211. muHex: data.mu.hex,
  212. };
  213. };