diff --git a/.vscode/settings.json b/.vscode/settings.json index 21800944..513d8583 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,7 @@ "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" diff --git a/package.json b/package.json index 7c96f737..117c2a5c 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "svelte-highlight": "^7.8.3", "svg-path-parser": "^1.1.0", "topojson-client": "^3.1.0", + "ts-essentials": "^10.0.4", "tslib": "^2.8.1", "typedoc": "^0.28.5", "typedoc-plugin-markdown": "^4.6.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac271d21..c9817632 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,6 +195,9 @@ importers: topojson-client: specifier: ^3.1.0 version: 3.1.0 + ts-essentials: + specifier: ^10.0.4 + version: 10.0.4(typescript@5.8.3) tslib: specifier: ^2.8.1 version: 2.8.1 @@ -2869,10 +2872,6 @@ packages: resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} engines: {node: '>=12.0.0'} - extend-shallow@2.0.1: - resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} - engines: {node: '>=0.10.0'} - extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -3075,10 +3074,6 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - gray-matter@4.0.3: - resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} - engines: {node: '>=6.0'} - gzip-size@6.0.0: resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} engines: {node: '>=10'} @@ -3430,10 +3425,6 @@ packages: resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} engines: {node: '>=0.10.0'} - kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} @@ -4149,10 +4140,6 @@ packages: search-insights@2.13.0: resolution: {integrity: sha512-Orrsjf9trHHxFRuo9/rzm0KIWmgzE8RMlZMzuhZOJ01Rnz3D0YBAe+V6473t6/H6c7irs6Lt48brULAiRWb3Vw==} - section-matter@1.0.0: - resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} - engines: {node: '>=4'} - semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -4315,10 +4302,6 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-bom-string@1.0.0: - resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} - engines: {node: '>=0.10.0'} - strip-comments@2.0.1: resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} engines: {node: '>=10'} @@ -4482,6 +4465,14 @@ packages: peerDependencies: typescript: '>=4.8.4' + ts-essentials@10.0.4: + resolution: {integrity: sha512-lwYdz28+S4nicm+jFi6V58LaAIpxzhg9rLdgNC1VsdP/xiFBseGhF1M/shwCk6zMmwahBZdXcl34LVHrEang3A==} + peerDependencies: + typescript: '>=4.5.0' + peerDependenciesMeta: + typescript: + optional: true + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -5135,7 +5126,7 @@ snapshots: '@babel/core': 7.25.2 '@babel/helper-compilation-targets': 7.25.2 '@babel/helper-plugin-utils': 7.27.1 - debug: 4.4.0 + debug: 4.4.1 lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -6751,12 +6742,12 @@ snapshots: '@typescript-eslint/type-utils': 7.7.0(eslint@9.27.0(jiti@1.21.0))(typescript@5.8.3) '@typescript-eslint/utils': 7.7.0(eslint@9.27.0(jiti@1.21.0))(typescript@5.8.3) '@typescript-eslint/visitor-keys': 7.7.0 - debug: 4.4.0 + debug: 4.4.1 eslint: 9.27.0(jiti@1.21.0) graphemer: 1.4.0 ignore: 5.3.1 natural-compare: 1.4.0 - semver: 7.6.3 + semver: 7.7.2 ts-api-utils: 1.3.0(typescript@5.8.3) optionalDependencies: typescript: 5.8.3 @@ -6786,7 +6777,7 @@ snapshots: '@typescript-eslint/types': 7.7.0 '@typescript-eslint/typescript-estree': 7.7.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 7.7.0 - debug: 4.4.0 + debug: 4.4.1 eslint: 9.27.0(jiti@1.21.0) optionalDependencies: typescript: 5.8.3 @@ -6819,7 +6810,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.7.0(typescript@5.8.3) '@typescript-eslint/utils': 7.7.0(eslint@9.27.0(jiti@1.21.0))(typescript@5.8.3) - debug: 4.4.0 + debug: 4.4.1 eslint: 9.27.0(jiti@1.21.0) ts-api-utils: 1.3.0(typescript@5.8.3) optionalDependencies: @@ -6846,11 +6837,11 @@ snapshots: dependencies: '@typescript-eslint/types': 7.7.0 '@typescript-eslint/visitor-keys': 7.7.0 - debug: 4.4.0 + debug: 4.4.1 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.6.3 + semver: 7.7.2 ts-api-utils: 1.3.0(typescript@5.8.3) optionalDependencies: typescript: 5.8.3 @@ -6880,7 +6871,7 @@ snapshots: '@typescript-eslint/types': 7.7.0 '@typescript-eslint/typescript-estree': 7.7.0(typescript@5.8.3) eslint: 9.27.0(jiti@1.21.0) - semver: 7.6.3 + semver: 7.7.2 transitivePeerDependencies: - supports-color - typescript @@ -7829,7 +7820,7 @@ snapshots: eslint-compat-utils@0.5.1(eslint@9.27.0(jiti@1.21.0)): dependencies: eslint: 9.27.0(jiti@1.21.0) - semver: 7.6.3 + semver: 7.7.2 eslint-config-prettier@10.1.5(eslint@9.27.0(jiti@1.21.0)): dependencies: @@ -7852,7 +7843,7 @@ snapshots: globals: 15.9.0 ignore: 5.3.1 minimatch: 9.0.5 - semver: 7.6.3 + semver: 7.7.2 eslint-plugin-svelte@3.9.0(eslint@9.27.0(jiti@1.21.0))(svelte@5.33.2): dependencies: @@ -7977,10 +7968,6 @@ snapshots: expect-type@1.2.1: {} - extend-shallow@2.0.1: - dependencies: - is-extendable: 0.1.1 - extend@3.0.2: {} extract-zip@2.0.1: @@ -8191,13 +8178,6 @@ snapshots: graphemer@1.4.0: {} - gray-matter@4.0.3: - dependencies: - js-yaml: 3.14.1 - kind-of: 6.0.3 - section-matter: 1.0.0 - strip-bom-string: 1.0.0 - gzip-size@6.0.0: dependencies: duplexer: 0.1.2 @@ -8540,8 +8520,6 @@ snapshots: dependencies: is-buffer: 1.1.6 - kind-of@6.0.3: {} - kleur@4.1.5: {} known-css-properties@0.36.0: {} @@ -9533,11 +9511,6 @@ snapshots: search-insights@2.13.0: {} - section-matter@1.0.0: - dependencies: - extend-shallow: 2.0.1 - kind-of: 6.0.3 - semver@6.3.1: {} semver@7.6.3: {} @@ -9735,8 +9708,6 @@ snapshots: dependencies: ansi-regex: 5.0.1 - strip-bom-string@1.0.0: {} - strip-comments@2.0.1: {} strip-final-newline@2.0.0: {} @@ -9905,6 +9876,10 @@ snapshots: dependencies: typescript: 5.8.3 + ts-essentials@10.0.4(typescript@5.8.3): + optionalDependencies: + typescript: 5.8.3 + tslib@2.8.1: {} tsx@4.16.5: @@ -10165,7 +10140,7 @@ snapshots: vite-plugin-pwa@0.19.0(vite@6.3.5(@types/node@20.10.8)(jiti@1.21.0)(sass@1.89.0)(terser@5.26.0)(tsx@4.16.5)(yaml@2.7.1))(workbox-build@7.0.0(@types/babel__core@7.20.5))(workbox-window@7.0.0): dependencies: - debug: 4.4.0 + debug: 4.4.1 fast-glob: 3.3.2 pretty-bytes: 6.1.1 vite: 6.3.5(@types/node@20.10.8)(jiti@1.21.0)(sass@1.89.0)(terser@5.26.0)(tsx@4.16.5)(yaml@2.7.1) diff --git a/src/lib/marks/AxisX.svelte b/src/lib/marks/AxisX.svelte index f786da3e..58fe7eea 100644 --- a/src/lib/marks/AxisX.svelte +++ b/src/lib/marks/AxisX.svelte @@ -2,6 +2,7 @@ Renders a horizontal axis with labels and tick marks --> + + + + + + + + diff --git a/src/routes/examples/axis/tick-interval.svelte b/src/routes/examples/axis/tick-interval.svelte new file mode 100644 index 00000000..25e50fa5 --- /dev/null +++ b/src/routes/examples/axis/tick-interval.svelte @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/src/routes/examples/axis/tick-spacing.svelte b/src/routes/examples/axis/tick-spacing.svelte new file mode 100644 index 00000000..594f0567 --- /dev/null +++ b/src/routes/examples/axis/tick-spacing.svelte @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/src/routes/marks/axis/+page.md b/src/routes/marks/axis/+page.md index 48b7c2f8..1b6d7502 100644 --- a/src/routes/marks/axis/+page.md +++ b/src/routes/marks/axis/+page.md @@ -260,6 +260,8 @@ You can explicitly add an x axis using the `AxisX` mark component. The `AxisX` c - `tickSize` - size of the tick marks in pixels (default: 6) - `tickFontSize` - font size for tick labels (default: 11) - `tickPadding` - padding between tick lines and labels (default: 3) +- `tickSpacing` - approximate pixel space between generated ticks +- `tickCount` - approximate number of ticks to generate - `tickFormat` - custom formatter for tick labels (can be 'auto', Intl.DateTimeFormatOptions, Intl.NumberFormatOptions, or custom function) - `tickClass` - function to assign custom classes to ticks based on their values - `automatic` - internal flag, set to true for implicit axes @@ -280,6 +282,8 @@ The `AxisY` component provides extensive customization options for y-axis presen - `tickSize` - size of the tick marks in pixels (default: 6) - `tickFontSize` - font size for tick labels (default: 11) - `tickPadding` - padding between tick lines and labels (default: 3) +- `tickSpacing` - approximate pixel space between generated ticks +- `tickCount` - approximate number of ticks to generate - `tickFormat` - custom formatter for tick labels (can be 'auto', Intl.DateTimeFormatOptions, Intl.NumberFormatOptions, or custom function) - `tickClass` - function to assign custom classes to ticks based on their values - `automatic` - internal flag, set to true for implicit axes diff --git a/src/tests/axisX.test.svelte b/src/tests/axisX.test.svelte new file mode 100644 index 00000000..44a0e8c0 --- /dev/null +++ b/src/tests/axisX.test.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/src/tests/axisX.test.ts b/src/tests/axisX.test.ts new file mode 100644 index 00000000..54ad92b4 --- /dev/null +++ b/src/tests/axisX.test.ts @@ -0,0 +1,111 @@ +import { describe, it, expect } from 'vitest'; +import { render } from '@testing-library/svelte'; +import AxisXTest from './axisX.test.svelte'; + +describe('AxisX mark', () => { + it('default axis', () => { + const { container } = render(AxisXTest, { + props: { + plotArgs: { width: 400, x: { domain: [0, 100] } } + } + }); + + const ticks = container.querySelectorAll('g.axis-x > g.tick') as NodeListOf; + const tickValues = Array.from(ticks).map((t) => t.querySelector('text')?.textContent); + expect(ticks.length).toBeGreaterThan(2); + expect(tickValues).toStrictEqual(['0', '20', '40', '60', '80', '100']); + }); + + it('custom tick values via axis.data', () => { + const { container } = render(AxisXTest, { + props: { + plotArgs: { width: 400, x: { domain: [0, 100] } }, + axisArgs: { data: [0, 20, 80] } + } + }); + + const ticks = container.querySelectorAll('g.axis-x > g.tick') as NodeListOf; + const tickValues = Array.from(ticks).map((t) => t.querySelector('text')?.textContent); + expect(ticks.length).toBe(3); + expect(tickValues).toStrictEqual(['0', '20', '80']); + }); + + it('custom tick values via x scale ticks options', () => { + const { container } = render(AxisXTest, { + props: { + plotArgs: { width: 400, x: { domain: [0, 100], ticks: [0, 20, 80] } } + } + }); + + const ticks = container.querySelectorAll('g.axis-x > g.tick') as NodeListOf; + const tickValues = Array.from(ticks).map((t) => t.querySelector('text')?.textContent); + expect(ticks.length).toBe(3); + expect(tickValues).toStrictEqual(['0', '20', '80']); + }); + + it('tickCount', () => { + const { container } = render(AxisXTest, { + props: { + plotArgs: { width: 400, x: { domain: [0, 100] } }, + axisArgs: { tickCount: 3 } + } + }); + + const ticks = container.querySelectorAll('g.axis-x > g.tick') as NodeListOf; + const tickValues = Array.from(ticks).map((t) => t.querySelector('text')?.textContent); + expect(ticks.length).toBe(3); + expect(tickValues).toStrictEqual(['0', '50', '100']); + }); + + it('tick spacing via axis options', () => { + const { container } = render(AxisXTest, { + props: { + plotArgs: { width: 400, x: { domain: [0, 100] } }, + axisArgs: { tickSpacing: 200 } + } + }); + + const ticks = container.querySelectorAll('g.axis-x > g.tick') as NodeListOf; + const tickValues = Array.from(ticks).map((t) => t.querySelector('text')?.textContent); + expect(ticks.length).toBe(3); + expect(tickValues).toStrictEqual(['0', '50', '100']); + }); + + it('tick spacing via scale options', () => { + const { container } = render(AxisXTest, { + props: { + plotArgs: { width: 400, x: { domain: [0, 100], tickSpacing: 200 } } + } + }); + + const ticks = container.querySelectorAll('g.axis-x > g.tick') as NodeListOf; + const tickValues = Array.from(ticks).map((t) => t.querySelector('text')?.textContent); + expect(ticks.length).toBe(3); + expect(tickValues).toStrictEqual(['0', '50', '100']); + }); + + it('tick interval via scale options', () => { + const { container } = render(AxisXTest, { + props: { + plotArgs: { width: 400, x: { domain: [0, 100], interval: 30 } } + } + }); + + const ticks = container.querySelectorAll('g.axis-x > g.tick') as NodeListOf; + const tickValues = Array.from(ticks).map((t) => t.querySelector('text')?.textContent); + expect(tickValues).toStrictEqual(['0', '30', '60', '90', '120']); + }); + + it('tick interval via axis options', () => { + const { container } = render(AxisXTest, { + props: { + plotArgs: { width: 400, x: { domain: [0, 100] } }, + axisArgs: { interval: 30 } + } + }); + + const ticks = container.querySelectorAll('g.axis-x > g.tick') as NodeListOf; + const tickValues = Array.from(ticks).map((t) => t.querySelector('text')?.textContent); + expect(tickValues).toStrictEqual(['0', '30', '60', '90', '120']); + }); +}); diff --git a/src/tests/axisY.test.svelte b/src/tests/axisY.test.svelte new file mode 100644 index 00000000..9fa595a7 --- /dev/null +++ b/src/tests/axisY.test.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/src/tests/axisY.test.ts b/src/tests/axisY.test.ts new file mode 100644 index 00000000..0b7f5b3c --- /dev/null +++ b/src/tests/axisY.test.ts @@ -0,0 +1,111 @@ +import { describe, it, expect } from 'vitest'; +import { render } from '@testing-library/svelte'; +import AxisYTest from './axisY.test.svelte'; + +describe('AxisY mark', () => { + it('default axis', () => { + const { container } = render(AxisYTest, { + props: { + plotArgs: { height: 300, y: { domain: [0, 100] } } + } + }); + + const ticks = container.querySelectorAll('g.axis-y > g.tick') as NodeListOf; + const tickValues = Array.from(ticks).map((t) => t.querySelector('text')?.textContent); + expect(ticks.length).toBeGreaterThan(2); + expect(tickValues).toStrictEqual(['0', '20', '40', '60', '80', '100']); + }); + + it('custom tick values via axis.data', () => { + const { container } = render(AxisYTest, { + props: { + plotArgs: { height: 300, y: { domain: [0, 100] } }, + axisArgs: { data: [0, 20, 80] } + } + }); + + const ticks = container.querySelectorAll('g.axis-y > g.tick') as NodeListOf; + const tickValues = Array.from(ticks).map((t) => t.querySelector('text')?.textContent); + expect(ticks.length).toBe(3); + expect(tickValues).toStrictEqual(['0', '20', '80']); + }); + + it('custom tick values via x scale ticks options', () => { + const { container } = render(AxisYTest, { + props: { + plotArgs: { height: 300, y: { domain: [0, 100], ticks: [0, 20, 80] } } + } + }); + + const ticks = container.querySelectorAll('g.axis-y > g.tick') as NodeListOf; + const tickValues = Array.from(ticks).map((t) => t.querySelector('text')?.textContent); + expect(ticks.length).toBe(3); + expect(tickValues).toStrictEqual(['0', '20', '80']); + }); + + it('tickCount', () => { + const { container } = render(AxisYTest, { + props: { + plotArgs: { height: 300, y: { domain: [0, 100] } }, + axisArgs: { tickCount: 3 } + } + }); + + const ticks = container.querySelectorAll('g.axis-y > g.tick') as NodeListOf; + const tickValues = Array.from(ticks).map((t) => t.querySelector('text')?.textContent); + expect(ticks.length).toBe(3); + expect(tickValues).toStrictEqual(['0', '50', '100']); + }); + + it('tickSpacing', () => { + const { container } = render(AxisYTest, { + props: { + plotArgs: { height: 300, y: { domain: [0, 100] } }, + axisArgs: { tickSpacing: 200 } + } + }); + + const ticks = container.querySelectorAll('g.axis-y > g.tick') as NodeListOf; + const tickValues = Array.from(ticks).map((t) => t.querySelector('text')?.textContent); + expect(ticks.length).toBe(3); + expect(tickValues).toStrictEqual(['0', '50', '100']); + }); + + it('tickSpacing', () => { + const { container } = render(AxisYTest, { + props: { + plotArgs: { height: 300, y: { domain: [0, 100], tickSpacing: 200 } } + } + }); + + const ticks = container.querySelectorAll('g.axis-y > g.tick') as NodeListOf; + const tickValues = Array.from(ticks).map((t) => t.querySelector('text')?.textContent); + expect(ticks.length).toBe(3); + expect(tickValues).toStrictEqual(['0', '50', '100']); + }); + + it('tick interval via scale options', () => { + const { container } = render(AxisYTest, { + props: { + plotArgs: { height: 300, y: { domain: [0, 100], interval: 30 } } + } + }); + + const ticks = container.querySelectorAll('g.axis-y > g.tick') as NodeListOf; + const tickValues = Array.from(ticks).map((t) => t.querySelector('text')?.textContent); + expect(tickValues).toStrictEqual(['0', '30', '60', '90', '120']); + }); + + it('tick interval via axis options', () => { + const { container } = render(AxisYTest, { + props: { + plotArgs: { height: 300, y: { domain: [0, 100] } }, + axisArgs: { interval: 30 } + } + }); + + const ticks = container.querySelectorAll('g.axis-y > g.tick') as NodeListOf; + const tickValues = Array.from(ticks).map((t) => t.querySelector('text')?.textContent); + expect(tickValues).toStrictEqual(['0', '30', '60', '90', '120']); + }); +}); diff --git a/static/examples/axis/tick-count.dark.png b/static/examples/axis/tick-count.dark.png new file mode 100644 index 00000000..57dd89b0 Binary files /dev/null and b/static/examples/axis/tick-count.dark.png differ diff --git a/static/examples/axis/tick-count.png b/static/examples/axis/tick-count.png new file mode 100644 index 00000000..a2801e9e Binary files /dev/null and b/static/examples/axis/tick-count.png differ diff --git a/static/examples/axis/tick-interval.dark.png b/static/examples/axis/tick-interval.dark.png new file mode 100644 index 00000000..c85ed1a5 Binary files /dev/null and b/static/examples/axis/tick-interval.dark.png differ diff --git a/static/examples/axis/tick-interval.png b/static/examples/axis/tick-interval.png new file mode 100644 index 00000000..a9a23c25 Binary files /dev/null and b/static/examples/axis/tick-interval.png differ diff --git a/static/examples/axis/tick-spacing.dark.png b/static/examples/axis/tick-spacing.dark.png new file mode 100644 index 00000000..e451e2de Binary files /dev/null and b/static/examples/axis/tick-spacing.dark.png differ diff --git a/static/examples/axis/tick-spacing.png b/static/examples/axis/tick-spacing.png new file mode 100644 index 00000000..bea52550 Binary files /dev/null and b/static/examples/axis/tick-spacing.png differ pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy