math.js 5.4 KB

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