|
7 | 7 |
|
8 | 8 | import { getObjectOrNull, type Accessor } from '$lib/utils/common.js'; |
9 | 9 | import type { MotionProp } from '$lib/utils/motion.svelte.js'; |
10 | | - import { type AnyScale, type DomainType } from '$lib/utils/scales.svelte.js'; |
| 10 | + import { type AnyScale, type DomainType, isScaleBand } from '$lib/utils/scales.svelte.js'; |
11 | 11 | import type { |
12 | 12 | BaseRange, |
13 | 13 | Nice, |
|
22 | 22 | import { geoFitObjectTransform } from '$lib/utils/geo.js'; |
23 | 23 | import TransformContext from './TransformContext.svelte'; |
24 | 24 | import BrushContext from './BrushContext.svelte'; |
25 | | - import { type BrushDomainType, type BrushState } from '$lib/states/brush.svelte.js'; |
| 25 | + import { type BrushDomainType, type BrushState, expandBandBrushDomain } from '$lib/states/brush.svelte.js'; |
26 | 26 |
|
27 | 27 | import { setChartContext } from '$lib/contexts/chart.js'; |
28 | 28 | import { ChartState } from '$lib/states/chart.svelte.js'; |
|
833 | 833 |
|
834 | 834 | const d0 = baseDomain[0] as unknown; |
835 | 835 | const d1 = baseDomain[1] as unknown; |
| 836 | +
|
| 837 | + // Skip domain extent constraint for categorical scales (range clamping handles boundaries) |
| 838 | + if (typeof d0 === 'string') return axisTranslate; |
836 | 839 | const isDate = d0 instanceof Date; |
837 | 840 | const rawD0 = isDate ? (d0 as Date).getTime() : (d0 as number); |
838 | 841 | const rawD1 = isDate ? (d1 as Date).getTime() : (d1 as number); |
|
925 | 928 | }; |
926 | 929 | }); |
927 | 930 |
|
928 | | - // Compose user-provided constrain with domainExtent constrain |
| 931 | + // Whether this is a band scale domain transform (affects scaleExtent and constrain defaults) |
| 932 | + const isBandDomainTransform = $derived( |
| 933 | + transform?.mode === 'domain' && ( |
| 934 | + ((transform.axis ?? 'both') !== 'y' && isScaleBand(chartState._xScaleProp)) || |
| 935 | + ((transform.axis ?? 'both') !== 'x' && isScaleBand(chartState._yScaleProp)) |
| 936 | + ) |
| 937 | + ); |
| 938 | +
|
| 939 | + // For band scale domain transforms, enforce scaleExtent minimum of 1 (can't zoom out past initial view) |
| 940 | + const resolvedScaleExtent = $derived.by(() => { |
| 941 | + if (!isBandDomainTransform) return transform?.scaleExtent; |
| 942 | + const userExtent = transform?.scaleExtent; |
| 943 | + return [Math.max(1, userExtent?.[0] ?? 1), userExtent?.[1] ?? Infinity] as [number, number]; |
| 944 | + }); |
| 945 | +
|
| 946 | + // Default constrain for band scale domain transforms: prevent panning past data boundaries |
| 947 | + const bandScaleConstrain = $derived.by(() => { |
| 948 | + if (!isBandDomainTransform) return undefined; |
| 949 | + const xIsBand = |
| 950 | + (transform!.axis ?? 'both') !== 'y' && isScaleBand(chartState._xScaleProp); |
| 951 | + const yIsBand = |
| 952 | + (transform!.axis ?? 'both') !== 'x' && isScaleBand(chartState._yScaleProp); |
| 953 | +
|
| 954 | + return (t: { scale: number; translate: { x: number; y: number } }) => { |
| 955 | + let { scale, translate } = t; |
| 956 | + let tx = translate.x; |
| 957 | + let ty = translate.y; |
| 958 | + if (xIsBand) { |
| 959 | + // translate.x must be in [width * (1 - scale), 0] |
| 960 | + tx = Math.max(chartState.width * (1 - scale), Math.min(0, tx)); |
| 961 | + } |
| 962 | + if (yIsBand) { |
| 963 | + ty = Math.max(chartState.height * (1 - scale), Math.min(0, ty)); |
| 964 | + } |
| 965 | + return { scale, translate: { x: tx, y: ty } }; |
| 966 | + }; |
| 967 | + }); |
| 968 | +
|
| 969 | + // Compose user-provided constrain with domainExtent constrain and band scale constrain |
929 | 970 | const composedConstrain = $derived.by(() => { |
930 | 971 | const userConstrain = transform?.constrain; |
931 | | - if (!domainExtentConstrain && !userConstrain) return undefined; |
932 | | - if (!domainExtentConstrain) return userConstrain; |
933 | | - if (!userConstrain) return domainExtentConstrain; |
934 | | - // Domain extent first, then user constrain |
| 972 | + const constrains = [bandScaleConstrain, domainExtentConstrain, userConstrain].filter(Boolean) as Array<(t: { scale: number; translate: { x: number; y: number } }) => { scale: number; translate: { x: number; y: number } }>; |
| 973 | + if (constrains.length === 0) return undefined; |
| 974 | + if (constrains.length === 1) return constrains[0]; |
935 | 975 | return (t: { scale: number; translate: { x: number; y: number } }) => { |
936 | | - return userConstrain(domainExtentConstrain(t)); |
| 976 | + return constrains.reduce((acc, fn) => fn(acc), t); |
937 | 977 | }; |
938 | 978 | }); |
939 | 979 |
|
|
954 | 994 | chartState.zoomToBrush(e.brush, userProps.axis ?? 'x'); |
955 | 995 | } else if (zoomOnBrush) { |
956 | 996 | const axis = userProps.axis ?? 'x'; |
957 | | - if (axis === 'x' || axis === 'both') brushXDomain = e.brush.x; |
958 | | - if (axis === 'y' || axis === 'both') brushYDomain = e.brush.y; |
| 997 | + if (axis === 'x' || axis === 'both') { |
| 998 | + brushXDomain = expandBandBrushDomain(e.brush.x, chartState._baseXDomain); |
| 999 | + } |
| 1000 | + if (axis === 'y' || axis === 'both') { |
| 1001 | + brushYDomain = expandBandBrushDomain(e.brush.y, chartState._baseYDomain); |
| 1002 | + } |
959 | 1003 | } |
960 | 1004 | userOnBrushEnd?.(e); |
961 | 1005 | e.brush.reset(); |
|
993 | 1037 | > |
994 | 1038 | {#key chartState.isMounted} |
995 | 1039 | <!-- svelte-ignore ownership_invalid_binding --> |
996 | | - {@const { domainExtent: _de, constrain: _uc, apply: _apply, ...transformProps } = transform ?? {}} |
| 1040 | + {@const { domainExtent: _de, constrain: _uc, apply: _apply, scaleExtent: _se, ...transformProps } = transform ?? {}} |
997 | 1041 | <TransformContext |
998 | 1042 | bind:state={chartState.transformState} |
999 | 1043 | mode={transform?.mode ?? 'none'} |
1000 | 1044 | initialTranslate={resolvedApply.translate ? initialTransform?.translate : undefined} |
1001 | 1045 | initialScale={resolvedApply.scale ? initialTransform?.scale : undefined} |
1002 | 1046 | {processTranslate} |
1003 | 1047 | {...transformProps} |
| 1048 | + scaleExtent={resolvedScaleExtent} |
1004 | 1049 | constrain={composedConstrain} |
1005 | 1050 | disablePointer={(brush === true || (typeof brush === 'object' && !brush.disabled)) || transform?.disablePointer} |
1006 | 1051 | {ondragstart} |
|
0 commit comments