|
1 |
| -import {extent} from "d3"; |
2 |
| -import {AxisX, AxisY} from "./axis.js"; |
3 |
| -import {formatDefault} from "./format.js"; |
4 |
| -import {isOrdinalScale, isTemporalScale, scaleOrder} from "./scales.js"; |
5 |
| -import {position, registry} from "./scales/index.js"; |
6 |
| - |
7 |
| -export function Axes( |
8 |
| - {x: xScale, y: yScale, fx: fxScale, fy: fyScale}, |
9 |
| - { |
10 |
| - x = {}, |
11 |
| - y = {}, |
12 |
| - fx = {}, |
13 |
| - fy = {}, |
14 |
| - axis = true, |
15 |
| - grid, |
16 |
| - line, |
17 |
| - label, |
18 |
| - facet: {axis: facetAxis = axis, grid: facetGrid, label: facetLabel = label} = {} |
19 |
| - } = {} |
20 |
| -) { |
21 |
| - let {axis: xAxis = axis} = x; |
22 |
| - let {axis: yAxis = axis} = y; |
23 |
| - let {axis: fxAxis = facetAxis} = fx; |
24 |
| - let {axis: fyAxis = facetAxis} = fy; |
25 |
| - if (!xScale) xAxis = null; |
26 |
| - else if (xAxis === true) xAxis = "bottom"; |
27 |
| - if (!yScale) yAxis = null; |
28 |
| - else if (yAxis === true) yAxis = "left"; |
29 |
| - if (!fxScale) fxAxis = null; |
30 |
| - else if (fxAxis === true) fxAxis = xAxis === "bottom" ? "top" : "bottom"; |
31 |
| - if (!fyScale) fyAxis = null; |
32 |
| - else if (fyAxis === true) fyAxis = yAxis === "left" ? "right" : "left"; |
33 |
| - return { |
34 |
| - ...(xAxis && {x: new AxisX(xScale, {grid, line, label, ...x, axis: xAxis})}), |
35 |
| - ...(yAxis && {y: new AxisY(yScale, {grid, line, label, ...y, axis: yAxis})}), |
36 |
| - ...(fxAxis && {fx: new AxisX(fxScale, {name: "fx", grid: facetGrid, label: facetLabel, ...fx, axis: fxAxis})}), |
37 |
| - ...(fyAxis && {fy: new AxisY(fyScale, {name: "fy", grid: facetGrid, label: facetLabel, ...fy, axis: fyAxis})}) |
38 |
| - }; |
39 |
| -} |
40 |
| - |
41 |
| -// Mutates axis.ticks! |
42 |
| -// TODO Populate tickFormat if undefined, too? |
43 |
| -export function autoAxisTicks({x, y, fx, fy}, {x: xAxis, y: yAxis, fx: fxAxis, fy: fyAxis}) { |
44 |
| - if (fxAxis) autoAxisTicksK(fx, fxAxis, 80); |
45 |
| - if (fyAxis) autoAxisTicksK(fy, fyAxis, 35); |
46 |
| - if (xAxis) autoAxisTicksK(x, xAxis, 80); |
47 |
| - if (yAxis) autoAxisTicksK(y, yAxis, 35); |
48 |
| -} |
49 |
| - |
50 |
| -function autoAxisTicksK(scale, axis, k) { |
51 |
| - if (axis.ticks === undefined) { |
52 |
| - const interval = scale.interval; |
53 |
| - if (interval !== undefined) { |
54 |
| - const [min, max] = extent(scale.scale.domain()); |
55 |
| - axis.ticks = interval.range(interval.floor(min), interval.offset(interval.floor(max))); |
56 |
| - } else { |
57 |
| - const [min, max] = extent(scale.scale.range()); |
58 |
| - axis.ticks = (max - min) / k; |
59 |
| - } |
60 |
| - } |
61 |
| - // D3’s ordinal scales simply use toString by default, but if the ordinal |
62 |
| - // scale domain (or ticks) are numbers or dates (say because we’re applying a |
63 |
| - // time interval to the ordinal scale), we want Plot’s default formatter. |
64 |
| - if (axis.tickFormat === undefined && isOrdinalScale(scale)) { |
65 |
| - axis.tickFormat = formatDefault; |
66 |
| - } |
67 |
| -} |
68 |
| - |
69 |
| -// Mutates axis.{label,labelAnchor,labelOffset} and scale.label! |
70 |
| -export function autoScaleLabels(channels, scales, {x, y, fx, fy}, dimensions, options) { |
71 |
| - if (fx) { |
72 |
| - autoAxisLabelsX(fx, scales.fx, channels.get("fx")); |
73 |
| - if (fx.labelOffset === undefined) { |
74 |
| - const {facetMarginTop, facetMarginBottom} = dimensions; |
75 |
| - fx.labelOffset = fx.axis === "top" ? facetMarginTop : facetMarginBottom; |
76 |
| - } |
77 |
| - } |
78 |
| - if (fy) { |
79 |
| - autoAxisLabelsY(fy, fx, scales.fy, channels.get("fy")); |
80 |
| - if (fy.labelOffset === undefined) { |
81 |
| - const {facetMarginLeft, facetMarginRight} = dimensions; |
82 |
| - fy.labelOffset = fy.axis === "left" ? facetMarginLeft : facetMarginRight; |
83 |
| - } |
84 |
| - } |
85 |
| - if (x) { |
86 |
| - autoAxisLabelsX(x, scales.x, channels.get("x")); |
87 |
| - if (x.labelOffset === undefined) { |
88 |
| - const {marginTop, marginBottom, facetMarginTop, facetMarginBottom} = dimensions; |
89 |
| - x.labelOffset = x.axis === "top" ? marginTop - facetMarginTop : marginBottom - facetMarginBottom; |
90 |
| - } |
91 |
| - } |
92 |
| - if (y) { |
93 |
| - autoAxisLabelsY(y, x, scales.y, channels.get("y")); |
94 |
| - if (y.labelOffset === undefined) { |
95 |
| - const {marginRight, marginLeft, facetMarginLeft, facetMarginRight} = dimensions; |
96 |
| - y.labelOffset = y.axis === "left" ? marginLeft - facetMarginLeft : marginRight - facetMarginRight; |
97 |
| - } |
98 |
| - } |
99 |
| - for (const [key, type] of registry) { |
100 |
| - if (type !== position && scales[key]) { |
101 |
| - // not already handled above |
102 |
| - autoScaleLabel(key, scales[key], channels.get(key), options[key]); |
103 |
| - } |
104 |
| - } |
105 |
| -} |
106 |
| - |
107 |
| -// Mutates axis.labelAnchor, axis.label, scale.label! |
108 |
| -function autoAxisLabelsX(axis, scale, channels) { |
109 |
| - if (axis.labelAnchor === undefined) { |
110 |
| - axis.labelAnchor = isOrdinalScale(scale) ? "center" : scaleOrder(scale) < 0 ? "left" : "right"; |
111 |
| - } |
112 |
| - if (axis.label === undefined) { |
113 |
| - axis.label = inferLabel(channels, scale, axis, "x"); |
114 |
| - } |
115 |
| - scale.label = axis.label; |
116 |
| -} |
117 |
| - |
118 |
| -// Mutates axis.labelAnchor, axis.label, scale.label! |
119 |
| -function autoAxisLabelsY(axis, opposite, scale, channels) { |
120 |
| - if (axis.labelAnchor === undefined) { |
121 |
| - axis.labelAnchor = isOrdinalScale(scale) |
122 |
| - ? "center" |
123 |
| - : opposite && opposite.axis === "top" |
124 |
| - ? "bottom" // TODO scaleOrder? |
125 |
| - : "top"; |
126 |
| - } |
127 |
| - if (axis.label === undefined) { |
128 |
| - axis.label = inferLabel(channels, scale, axis, "y"); |
129 |
| - } |
130 |
| - scale.label = axis.label; |
131 |
| -} |
132 |
| - |
133 |
| -// Mutates scale.label! |
134 |
| -function autoScaleLabel(key, scale, channels, options) { |
135 |
| - if (options) { |
136 |
| - scale.label = options.label; |
137 |
| - } |
138 |
| - if (scale.label === undefined) { |
139 |
| - scale.label = inferLabel(channels, scale, null, key); |
140 |
| - } |
141 |
| -} |
142 |
| - |
143 |
| -// Channels can have labels; if all the channels for a given scale are |
144 |
| -// consistently labeled (i.e., have the same value if not undefined), and the |
145 |
| -// corresponding axis doesn’t already have an explicit label, then the channels’ |
146 |
| -// label is promoted to the corresponding axis. |
147 |
| -function inferLabel(channels = [], scale, axis, key) { |
148 |
| - let candidate; |
149 |
| - for (const {label} of channels) { |
150 |
| - if (label === undefined) continue; |
151 |
| - if (candidate === undefined) candidate = label; |
152 |
| - else if (candidate !== label) return; |
153 |
| - } |
154 |
| - if (candidate !== undefined) { |
155 |
| - // Ignore the implicit label for temporal scales if it’s simply “date”. |
156 |
| - if (isTemporalScale(scale) && /^(date|time|year)$/i.test(candidate)) return; |
157 |
| - if (!isOrdinalScale(scale)) { |
158 |
| - if (scale.percent) candidate = `${candidate} (%)`; |
159 |
| - if (key === "x" || key === "y") { |
160 |
| - const order = scaleOrder(scale); |
161 |
| - if (order) { |
162 |
| - if (key === "x" || (axis && axis.labelAnchor === "center")) { |
163 |
| - candidate = (key === "x") === order < 0 ? `← ${candidate}` : `${candidate} →`; |
164 |
| - } else { |
165 |
| - candidate = `${order < 0 ? "↑ " : "↓ "}${candidate}`; |
166 |
| - } |
167 |
| - } |
168 |
| - } |
169 |
| - } |
170 |
| - } |
171 |
| - return candidate; |
| 1 | +import {format, utcFormat} from "d3"; |
| 2 | +import {formatIsoDate} from "./format.js"; |
| 3 | +import {constant, isTemporal, string} from "./options.js"; |
| 4 | +import {isOrdinalScale} from "./scales.js"; |
| 5 | + |
| 6 | +export function inferFontVariant(scale) { |
| 7 | + return isOrdinalScale(scale) && scale.interval === undefined ? undefined : "tabular-nums"; |
| 8 | +} |
| 9 | + |
| 10 | +// D3 doesn’t provide a tick format for ordinal scales; we want shorthand when |
| 11 | +// an ordinal domain is numbers or dates, and we want null to mean the empty |
| 12 | +// string, not the default identity format. TODO Remove this in favor of the |
| 13 | +// axis mark’s inferTickFormat. |
| 14 | +export function maybeAutoTickFormat(tickFormat, domain) { |
| 15 | + return tickFormat === undefined |
| 16 | + ? isTemporal(domain) |
| 17 | + ? formatIsoDate |
| 18 | + : string |
| 19 | + : typeof tickFormat === "function" |
| 20 | + ? tickFormat |
| 21 | + : (typeof tickFormat === "string" ? (isTemporal(domain) ? utcFormat : format) : constant)(tickFormat); |
172 | 22 | }
|
0 commit comments