math.js 5.4 KB

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