dimensions.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. import {extent} from "d3";
  2. import {projectionAspectRatio} from "./projection.js";
  3. import {isOrdinalScale} from "./scales.js";
  4. import {offset} from "./style.js";
  5. export function createDimensions(scales, marks, options = {}) {
  6. // Compute the default margins: the maximum of the marks’ margins. While not
  7. // always used, they may be needed to compute the default height of the plot.
  8. let marginTopDefault = 0.5 - offset,
  9. marginRightDefault = 0.5 + offset,
  10. marginBottomDefault = 0.5 + offset,
  11. marginLeftDefault = 0.5 - offset;
  12. for (const {marginTop, marginRight, marginBottom, marginLeft} of marks) {
  13. if (marginTop > marginTopDefault) marginTopDefault = marginTop;
  14. if (marginRight > marginRightDefault) marginRightDefault = marginRight;
  15. if (marginBottom > marginBottomDefault) marginBottomDefault = marginBottom;
  16. if (marginLeft > marginLeftDefault) marginLeftDefault = marginLeft;
  17. }
  18. // Compute the actual margins. The order of precedence is: the side-specific
  19. // margin options, then the global margin option, then the defaults.
  20. let {
  21. margin,
  22. marginTop = margin !== undefined ? margin : marginTopDefault,
  23. marginRight = margin !== undefined ? margin : marginRightDefault,
  24. marginBottom = margin !== undefined ? margin : marginBottomDefault,
  25. marginLeft = margin !== undefined ? margin : marginLeftDefault
  26. } = options;
  27. // Coerce the margin options to numbers.
  28. marginTop = +marginTop;
  29. marginRight = +marginRight;
  30. marginBottom = +marginBottom;
  31. marginLeft = +marginLeft;
  32. // Compute the outer dimensions of the plot. If the top and bottom margins are
  33. // specified explicitly, adjust the automatic height accordingly.
  34. let {
  35. width = 640,
  36. height = autoHeight(scales, options, {
  37. width,
  38. marginTopDefault,
  39. marginRightDefault,
  40. marginBottomDefault,
  41. marginLeftDefault
  42. }) + Math.max(0, marginTop - marginTopDefault + marginBottom - marginBottomDefault)
  43. } = options;
  44. // Coerce the width and height.
  45. width = +width;
  46. height = +height;
  47. const dimensions = {
  48. width,
  49. height,
  50. marginTop,
  51. marginRight,
  52. marginBottom,
  53. marginLeft
  54. };
  55. // Compute the facet margins.
  56. if (scales.fx || scales.fy) {
  57. let {
  58. margin: facetMargin,
  59. marginTop: facetMarginTop = facetMargin !== undefined ? facetMargin : marginTop,
  60. marginRight: facetMarginRight = facetMargin !== undefined ? facetMargin : marginRight,
  61. marginBottom: facetMarginBottom = facetMargin !== undefined ? facetMargin : marginBottom,
  62. marginLeft: facetMarginLeft = facetMargin !== undefined ? facetMargin : marginLeft
  63. } = options.facet ?? {};
  64. // Coerce the facet margin options to numbers.
  65. facetMarginTop = +facetMarginTop;
  66. facetMarginRight = +facetMarginRight;
  67. facetMarginBottom = +facetMarginBottom;
  68. facetMarginLeft = +facetMarginLeft;
  69. dimensions.facet = {
  70. marginTop: facetMarginTop,
  71. marginRight: facetMarginRight,
  72. marginBottom: facetMarginBottom,
  73. marginLeft: facetMarginLeft
  74. };
  75. }
  76. return dimensions;
  77. }
  78. function autoHeight(
  79. {x, y, fy, fx},
  80. {projection, aspectRatio},
  81. {width, marginTopDefault, marginRightDefault, marginBottomDefault, marginLeftDefault}
  82. ) {
  83. const nfy = fy ? fy.scale.domain().length || 1 : 1;
  84. // If a projection is specified, compute an aspect ratio based on the domain,
  85. // defaulting to the projection’s natural aspect ratio (if known).
  86. const ar = projectionAspectRatio(projection);
  87. if (ar) {
  88. const nfx = fx ? fx.scale.domain().length : 1;
  89. const far = ((1.1 * nfy - 0.1) / (1.1 * nfx - 0.1)) * ar; // 0.1 is default facet padding
  90. const lar = Math.max(0.1, Math.min(10, far)); // clamp the aspect ratio to a “reasonable” value
  91. return Math.round((width - marginLeftDefault - marginRightDefault) * lar + marginTopDefault + marginBottomDefault);
  92. }
  93. const ny = y ? (isOrdinalScale(y) ? y.scale.domain().length || 1 : Math.max(7, 17 / nfy)) : 1;
  94. // If a desired aspect ratio is given, compute a default height to match.
  95. if (aspectRatio != null) {
  96. aspectRatio = +aspectRatio;
  97. if (!(isFinite(aspectRatio) && aspectRatio > 0)) throw new Error(`invalid aspectRatio: ${aspectRatio}`);
  98. const ratio = aspectRatioLength("y", y) / (aspectRatioLength("x", x) * aspectRatio);
  99. const fxb = fx ? fx.scale.bandwidth() : 1;
  100. const fyb = fy ? fy.scale.bandwidth() : 1;
  101. const w = fxb * (width - marginLeftDefault - marginRightDefault) - x.insetLeft - x.insetRight;
  102. return (ratio * w + y.insetTop + y.insetBottom) / fyb + marginTopDefault + marginBottomDefault;
  103. }
  104. return !!(y || fy) * Math.max(1, Math.min(60, ny * nfy)) * 20 + !!fx * 30 + 60;
  105. }
  106. function aspectRatioLength(k, scale) {
  107. if (!scale) throw new Error(`aspectRatio requires ${k} scale`);
  108. const {type, domain} = scale;
  109. let transform;
  110. switch (type) {
  111. case "linear":
  112. case "utc":
  113. case "time":
  114. transform = Number;
  115. break;
  116. case "pow": {
  117. const exponent = scale.scale.exponent();
  118. transform = (x) => Math.pow(x, exponent);
  119. break;
  120. }
  121. case "log":
  122. transform = Math.log;
  123. break;
  124. case "point":
  125. case "band":
  126. return domain.length;
  127. default:
  128. throw new Error(`unsupported ${k} scale for aspectRatio: ${type}`);
  129. }
  130. const [min, max] = extent(domain);
  131. return Math.abs(transform(max) - transform(min));
  132. }