|
| 1 | +// Based on https://github.com/d3/d3-axis/blob/main/src/axis.js, |
| 2 | +// which is licensed under the ISC License (ISC) |
| 3 | +// Modified to allow disabling the domain line and tick lines |
| 4 | +// biome-ignore lint: |
| 5 | +type ANY = any; |
| 6 | + |
| 7 | +const identity = (d: ANY) => d; |
| 8 | +const top = 1; |
| 9 | +const right = 2; |
| 10 | +const bottom = 3; |
| 11 | +const left = 4; |
| 12 | +const epsilon = 1e-6; |
| 13 | + |
| 14 | +const translateX = (x: number) => `translate(${x},0)`; |
| 15 | +const translateY = (y: number) => `translate(0,${y})`; |
| 16 | +const number = (scale: ANY) => (d: ANY) => +scale(d); |
| 17 | + |
| 18 | +function center(scale: ANY, offset: ANY) { |
| 19 | + let _offset = Math.max(0, scale.bandwidth() - offset * 2) / 2; |
| 20 | + if (scale.round()) _offset = Math.round(_offset); |
| 21 | + return (d: ANY) => +scale(d) + _offset; |
| 22 | +} |
| 23 | + |
| 24 | +function entering() { |
| 25 | + // @ts-expect-error |
| 26 | + return !this.__axis; |
| 27 | +} |
| 28 | + |
| 29 | +function axis<Domain>(orient: number, scale: ANY) { |
| 30 | + type Axis = typeof axis; |
| 31 | + |
| 32 | + let tickArguments: unknown[] = []; |
| 33 | + let tickValues: ANY = null; |
| 34 | + let tickFormat: ANY = null; |
| 35 | + let tickSizeInner = 6; |
| 36 | + let tickSizeOuter = 6; |
| 37 | + let tickPadding = 3; |
| 38 | + let offset = typeof window !== "undefined" && window.devicePixelRatio > 1 ? 0 : 0.5; |
| 39 | + const k = orient === top || orient === left ? -1 : 1; |
| 40 | + const x = orient === left || orient === right ? "x" : "y"; |
| 41 | + const transform = orient === top || orient === bottom ? translateX : translateY; |
| 42 | + |
| 43 | + let disableDomain = false; |
| 44 | + let disableTicks = false; |
| 45 | + |
| 46 | + function axis(context: ANY) { |
| 47 | + const values = |
| 48 | + tickValues == null ? (scale.ticks ? scale.ticks.apply(scale, tickArguments) : scale.domain()) : tickValues; |
| 49 | + const format = |
| 50 | + tickFormat == null ? (scale.tickFormat ? scale.tickFormat.apply(scale, tickArguments) : identity) : tickFormat; |
| 51 | + const spacing = Math.max(tickSizeInner, 0) + tickPadding; |
| 52 | + const range = scale.range(); |
| 53 | + const range0 = +range[0] + offset; |
| 54 | + const range1 = +range[range.length - 1] + offset; |
| 55 | + const position = (scale.bandwidth ? center : number)(scale.copy(), offset); |
| 56 | + const selection = context.selection ? context.selection() : context; |
| 57 | + let path = selection.selectAll(".domain").data([null]); |
| 58 | + let tick = selection.selectAll(".tick").data(values, scale).order(); |
| 59 | + let tickExit = tick.exit(); |
| 60 | + const tickEnter = tick.enter().append("g").attr("class", "tick"); |
| 61 | + let line = tick.select("line"); |
| 62 | + let text = tick.select("text"); |
| 63 | + |
| 64 | + if (!disableDomain) |
| 65 | + path = path.merge(path.enter().insert("path", ".tick").attr("class", "domain").attr("stroke", "currentColor")); |
| 66 | + |
| 67 | + tick = tick.merge(tickEnter); |
| 68 | + |
| 69 | + if (!disableTicks) |
| 70 | + line = line.merge( |
| 71 | + tickEnter |
| 72 | + .append("line") |
| 73 | + .attr("stroke", "currentColor") |
| 74 | + .attr(`${x}2`, k * tickSizeInner), |
| 75 | + ); |
| 76 | + |
| 77 | + text = text.merge( |
| 78 | + tickEnter |
| 79 | + .append("text") |
| 80 | + .attr("fill", "currentColor") |
| 81 | + .attr(x, k * spacing) |
| 82 | + .attr("dy", orient === top ? "0em" : orient === bottom ? "0.71em" : "0.32em"), |
| 83 | + ); |
| 84 | + |
| 85 | + if (context !== selection) { |
| 86 | + path = path.transition(context); |
| 87 | + tick = tick.transition(context); |
| 88 | + line = line.transition(context); |
| 89 | + text = text.transition(context); |
| 90 | + |
| 91 | + tickExit = tickExit |
| 92 | + .transition(context) |
| 93 | + .attr("opacity", epsilon) |
| 94 | + .attr("transform", function (this: ANY, d: ANY) { |
| 95 | + const _d = position(d); |
| 96 | + return Number.isFinite(_d) ? transform(_d + offset) : <ANY>this.getAttribute("transform"); |
| 97 | + }); |
| 98 | + |
| 99 | + tickEnter.attr("opacity", epsilon).attr("transform", function (this: ANY, d: ANY) { |
| 100 | + let p = <ANY>this.parentNode.__axis; |
| 101 | + let t = 0; |
| 102 | + |
| 103 | + if (p) { |
| 104 | + p = p(d); |
| 105 | + t = Number.isFinite(p) ? p : position(d); |
| 106 | + } |
| 107 | + |
| 108 | + return transform(t + offset); |
| 109 | + }); |
| 110 | + } |
| 111 | + |
| 112 | + tickExit.remove(); |
| 113 | + |
| 114 | + path.attr( |
| 115 | + "d", |
| 116 | + orient === left || orient === right |
| 117 | + ? tickSizeOuter |
| 118 | + ? `M${k * tickSizeOuter},${range0}H${offset}V${range1}H${k * tickSizeOuter}` |
| 119 | + : `M${offset},${range0}V${range1}` |
| 120 | + : tickSizeOuter |
| 121 | + ? `M${range0},${k * tickSizeOuter}V${offset}H${range1}V${k * tickSizeOuter}` |
| 122 | + : `M${range0},${offset}H${range1}`, |
| 123 | + ); |
| 124 | + |
| 125 | + tick.attr("opacity", 1).attr("transform", (d: ANY) => transform(position(d) + offset)); |
| 126 | + |
| 127 | + line.attr(`${x}2`, k * tickSizeInner); |
| 128 | + |
| 129 | + text.attr(x, k * spacing).text(format); |
| 130 | + |
| 131 | + selection |
| 132 | + .filter(entering) |
| 133 | + .attr("fill", "none") |
| 134 | + .attr("font-size", 10) |
| 135 | + .attr("font-family", "sans-serif") |
| 136 | + .attr("text-anchor", orient === right ? "start" : orient === left ? "end" : "middle"); |
| 137 | + |
| 138 | + selection.each(function (this: ANY) { |
| 139 | + this.__axis = position; |
| 140 | + }); |
| 141 | + } |
| 142 | + |
| 143 | + axis.disableDomain = () => { |
| 144 | + disableDomain = true; |
| 145 | + return axis as Axis; |
| 146 | + }; |
| 147 | + |
| 148 | + axis.disableTicks = () => { |
| 149 | + disableTicks = true; |
| 150 | + return axis as Axis; |
| 151 | + }; |
| 152 | + |
| 153 | + axis.ticks = (...args: ANY[]) => { |
| 154 | + tickArguments = args; |
| 155 | + return axis as Axis; |
| 156 | + }; |
| 157 | + |
| 158 | + axis.tickArguments = (...args: ANY[]) => { |
| 159 | + if (args.length) { |
| 160 | + tickArguments = args[0] == null ? [] : Array.from(args[0]); |
| 161 | + return axis as Axis; |
| 162 | + } |
| 163 | + return tickArguments.slice(); |
| 164 | + }; |
| 165 | + |
| 166 | + axis.tickValues = (...args: ANY[]) => { |
| 167 | + if (args.length) { |
| 168 | + tickValues = args[0] == null ? null : Array.from(args[0]); |
| 169 | + return axis as Axis; |
| 170 | + } |
| 171 | + return tickValues?.slice(); |
| 172 | + }; |
| 173 | + |
| 174 | + axis.tickFormat = (format: (domainValue: Domain, index: number) => string) => { |
| 175 | + tickFormat = format; |
| 176 | + return axis as Axis; |
| 177 | + }; |
| 178 | + |
| 179 | + axis.tickSize = (...args: ANY[]) => { |
| 180 | + if (args.length) { |
| 181 | + tickSizeInner = tickSizeOuter = +args[0]; |
| 182 | + return axis as Axis; |
| 183 | + } |
| 184 | + |
| 185 | + return tickSizeInner; |
| 186 | + }; |
| 187 | + |
| 188 | + axis.tickSizeInner = (...args: ANY[]) => { |
| 189 | + if (args.length) { |
| 190 | + tickSizeInner = +args[0]; |
| 191 | + return axis as Axis; |
| 192 | + } |
| 193 | + return tickSizeInner; |
| 194 | + }; |
| 195 | + |
| 196 | + axis.tickSizeOuter = (...args: ANY[]) => { |
| 197 | + if (args.length) { |
| 198 | + tickSizeOuter = +args[0]; |
| 199 | + return axis as Axis; |
| 200 | + } |
| 201 | + return tickSizeOuter; |
| 202 | + }; |
| 203 | + |
| 204 | + axis.tickPadding = (...args: ANY[]) => { |
| 205 | + if (args.length) { |
| 206 | + tickPadding = +args[0]; |
| 207 | + return axis as Axis; |
| 208 | + } |
| 209 | + return tickPadding; |
| 210 | + }; |
| 211 | + |
| 212 | + axis.offset = (...args: ANY[]) => { |
| 213 | + if (args.length) { |
| 214 | + offset = +args[0]; |
| 215 | + return axis as Axis; |
| 216 | + } |
| 217 | + return offset; |
| 218 | + }; |
| 219 | + |
| 220 | + return axis as Axis; |
| 221 | +} |
| 222 | + |
| 223 | +export type AxisDomain = number | string | Date | { valueOf(): number }; |
| 224 | +export interface AxisScale<Domain> { |
| 225 | + (x: Domain): number | undefined; |
| 226 | + domain(): Domain[]; |
| 227 | + range(): number[]; |
| 228 | + copy(): this; |
| 229 | + bandwidth?(): number; |
| 230 | +} |
| 231 | + |
| 232 | +export function axisTop<Domain extends AxisDomain>(scale: AxisScale<Domain>) { |
| 233 | + return axis<Domain>(top, scale); |
| 234 | +} |
| 235 | + |
| 236 | +export function axisRight<Domain extends AxisDomain>(scale: AxisScale<Domain>) { |
| 237 | + return axis<Domain>(right, scale); |
| 238 | +} |
| 239 | + |
| 240 | +export function axisBottom<Domain extends AxisDomain>(scale: AxisScale<Domain>) { |
| 241 | + return axis<Domain>(bottom, scale); |
| 242 | +} |
| 243 | + |
| 244 | +export function axisLeft<Domain extends AxisDomain>(scale: AxisScale<Domain>) { |
| 245 | + return axis<Domain>(left, scale); |
| 246 | +} |
0 commit comments