math.js 4.2 KB

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