diff --git a/docs/features/formats.md b/docs/features/formats.md
index 8455dd5902..36d65d0bfc 100644
--- a/docs/features/formats.md
+++ b/docs/features/formats.md
@@ -9,6 +9,14 @@ import * as d3 from "d3";
These helper functions are provided for convenience as a **tickFormat** option for the [axis mark](../marks/axis.md), as the **text** option for a [text mark](../marks/text.md), or other use. See also [d3-format](https://d3js.org/d3-format), [d3-time-format](https://d3js.org/d3-time-format), and JavaScript’s built-in [date formatting](https://observablehq.com/@mbostock/date-formatting) and [number formatting](https://observablehq.com/@mbostock/number-formatting).
+## formatNumber(*locale*) {#formatNumber}
+
+```js
+Plot.formatNumber("en-US")(Math.PI) // "3.142"
+```
+
+Returns a function that formats a given number according to the specified *locale*. The *locale* is a [BCP 47 language tag](https://tools.ietf.org/html/bcp47) and defaults to U.S. English.
+
## formatIsoDate(*date*) {#formatIsoDate}
```js
diff --git a/src/format.d.ts b/src/format.d.ts
index 160041cd36..c073bfa07b 100644
--- a/src/format.d.ts
+++ b/src/format.d.ts
@@ -1,3 +1,13 @@
+/**
+ * Returns a function that formats a given number according to the specified
+ * *locale*.
+ *
+ * [1]: https://tools.ietf.org/html/bcp47
+ *
+ * @param locale - a [BCP 47 language tag][1]; defaults to U.S. English.
+ */
+export function formatNumber(locale?: string): (i: number) => string;
+
/**
* Returns a function that formats a given month number (from 0 = January to 11
* = December) according to the specified *locale* and *format*.
diff --git a/src/index.js b/src/index.js
index 9d8c162260..c3a87668ae 100644
--- a/src/index.js
+++ b/src/index.js
@@ -53,6 +53,6 @@ export {select, selectFirst, selectLast, selectMaxX, selectMaxY, selectMinX, sel
export {stackX, stackX1, stackX2, stackY, stackY1, stackY2} from "./transforms/stack.js";
export {treeNode, treeLink} from "./transforms/tree.js";
export {pointer, pointerX, pointerY} from "./interactions/pointer.js";
-export {formatIsoDate, formatWeekday, formatMonth} from "./format.js";
+export {formatIsoDate, formatNumber, formatWeekday, formatMonth} from "./format.js";
export {scale} from "./scales.js";
export {legend} from "./legends.js";
diff --git a/src/marks/axis.js b/src/marks/axis.js
index a36f1f6aa3..406ff70dd5 100644
--- a/src/marks/axis.js
+++ b/src/marks/axis.js
@@ -652,7 +652,7 @@ function inferTextChannel(scale, data, ticks, tickFormat, anchor) {
// possible, or the default ISO format (2014-01-26). TODO We need a better way
// to infer whether the ordinal scale is UTC or local time.
export function inferTickFormat(scale, data, ticks, tickFormat, anchor) {
- return typeof tickFormat === "function"
+ return typeof tickFormat === "function" && !(scale.type === "log" && scale.tickFormat)
? tickFormat
: tickFormat === undefined && data && isTemporal(data)
? inferTimeFormat(scale.type, data, anchor) ?? formatDefault
diff --git a/test/marks/format-test.js b/test/marks/format-test.js
index 3dad221547..2687778589 100644
--- a/test/marks/format-test.js
+++ b/test/marks/format-test.js
@@ -7,8 +7,7 @@ it("formatMonth(locale, format) does the right thing", () => {
assert.strictEqual(Plot.formatMonth("en", "narrow")(0), "J");
});
-// GitHub Actions does not support locales.
-it.skip("formatMonth('fr', format) does the right thing", () => {
+it("formatMonth('fr', format) does the right thing", () => {
assert.strictEqual(Plot.formatMonth("fr", "long")(11), "décembre");
assert.strictEqual(Plot.formatMonth("fr", "short")(11), "déc.");
assert.strictEqual(Plot.formatMonth("fr", "narrow")(11), "D");
@@ -27,7 +26,7 @@ it("formatMonth(locale) has the expected default", () => {
assert.strictEqual(Plot.formatMonth("en", undefined)(0), "Jan");
});
-it.skip("formatMonth('fr') has the expected default", () => {
+it("formatMonth('fr') has the expected default", () => {
assert.strictEqual(Plot.formatMonth("fr")(11), "déc.");
assert.strictEqual(Plot.formatMonth("fr", undefined)(11), "déc.");
});
diff --git a/test/output/logTickFormatFunction.svg b/test/output/logTickFormatFunction.svg
new file mode 100644
index 0000000000..f8e3c26905
--- /dev/null
+++ b/test/output/logTickFormatFunction.svg
@@ -0,0 +1,82 @@
+
\ No newline at end of file
diff --git a/test/output/logTickFormatFunctionSv.svg b/test/output/logTickFormatFunctionSv.svg
new file mode 100644
index 0000000000..628d7f37d2
--- /dev/null
+++ b/test/output/logTickFormatFunctionSv.svg
@@ -0,0 +1,82 @@
+
\ No newline at end of file
diff --git a/test/plot.js b/test/plot.js
index 57247af187..83fd9301ae 100644
--- a/test/plot.js
+++ b/test/plot.js
@@ -19,7 +19,7 @@ for (const [name, plot] of Object.entries(plots)) {
reindexMarker(root);
reindexClip(root);
let expected;
- let actual = beautify.html(root.outerHTML, {
+ let actual = beautify.html(root.outerHTML.replaceAll(" ", "\xa0"), {
indent_size: 2,
inline: ["title", "tspan", "span", "svg", "a", "i"],
indent_inner_html: false
diff --git a/test/plots/index.ts b/test/plots/index.ts
index 13231ea454..907109e590 100644
--- a/test/plots/index.ts
+++ b/test/plots/index.ts
@@ -152,6 +152,7 @@ export * from "./linear-regression-cars.js";
export * from "./linear-regression-mtcars.js";
export * from "./linear-regression-penguins.js";
export * from "./log-degenerate.js";
+export * from "./log-tick-format.js";
export * from "./long-labels.js";
export * from "./markers.js";
export * from "./markov-chain.js";
diff --git a/test/plots/log-tick-format.ts b/test/plots/log-tick-format.ts
new file mode 100644
index 0000000000..1bc5e27503
--- /dev/null
+++ b/test/plots/log-tick-format.ts
@@ -0,0 +1,9 @@
+import * as Plot from "@observablehq/plot";
+
+export async function logTickFormatFunction() {
+ return Plot.plot({x: {type: "log", domain: [1, 4200], tickFormat: Plot.formatNumber()}});
+}
+
+export async function logTickFormatFunctionSv() {
+ return Plot.plot({x: {type: "log", domain: [1, 4200], tickFormat: Plot.formatNumber("sv-SE")}});
+}