Skip to content

Commit c1932c6

Browse files
authored
Merge pull request #574 from techniq:axis-multiline-placement
fix(Axis): Correctly place multiline parts based on placement
2 parents 34c2103 + 4d5256f commit c1932c6

5 files changed

Lines changed: 86 additions & 52 deletions

File tree

.changeset/free-teeth-live.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'layerchart': patch
3+
---
4+
5+
fix(Axis): Correctly place multiline parts based on placement

packages/layerchart/src/lib/components/Axis.svelte

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,16 @@
207207
: undefined
208208
);
209209
const tickVals = $derived(resolveTickVals(scale, ticks, tickCount));
210-
const tickFormat = $derived(resolveTickFormat(scale, ticks, tickCount, format, tickMultiline));
210+
const tickFormat = $derived(
211+
resolveTickFormat({
212+
scale,
213+
ticks,
214+
count: tickCount,
215+
formatType: format,
216+
multiline: tickMultiline,
217+
placement,
218+
})
219+
);
211220
212221
function getCoords(tick: any) {
213222
switch (placement) {
@@ -407,6 +416,7 @@
407416
value: tickFormat(tick, index),
408417
...getDefaultTickLabelProps(tick),
409418
motion,
419+
lineHeight: '11px', // complement 10px text (until Text supports custom styles)
410420
...tickLabelProps,
411421
class: cls(
412422
layerClass('axis-tick-label'),

packages/layerchart/src/lib/components/Text.svelte

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -345,16 +345,6 @@
345345
}
346346
});
347347
348-
const pathStartDy = $derived.by(() => {
349-
if (verticalAnchor === 'start') {
350-
return getPixelValue(capHeight);
351-
} else if (verticalAnchor === 'middle') {
352-
return (0 / 2) * -getPixelValue(lineHeight) + getPixelValue(capHeight) / 2;
353-
} else {
354-
return 0 * -getPixelValue(lineHeight);
355-
}
356-
});
357-
358348
const scaleTransform = $derived.by(() => {
359349
if (
360350
scaleToFit &&
@@ -537,7 +527,7 @@
537527
{#each wordsByLines as line, index}
538528
<tspan
539529
x={motionX.current}
540-
dy={index === 0 ? startDy : lineHeight}
530+
dy={index === 0 ? startDy : getPixelValue(lineHeight)}
541531
class={layerClass('text-tspan')}
542532
>
543533
{line.words.join(' ')}

packages/layerchart/src/lib/utils/ticks.ts

Lines changed: 68 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,71 +9,78 @@ import {
99
DateToken,
1010
} from '@layerstack/utils';
1111
import { isScaleBand, isScaleTime, type AnyScale } from './scales.svelte.js';
12+
import type { AxisProps } from '$lib/components/Axis.svelte';
13+
14+
export function getDurationFormat(
15+
duration: Duration,
16+
options: { multiline?: boolean; placement?: AxisProps['placement'] } = {
17+
multiline: false,
18+
}
19+
) {
20+
const { multiline = false, placement = 'bottom' } = options;
1221

13-
export function getDurationFormat(duration: Duration, multiline = false) {
1422
return function (date: Date, i: number) {
23+
let result: string | Array<string | false> = '';
24+
1525
if (+duration >= +new Duration({ duration: { years: 1 } })) {
1626
// Year
17-
return format(date, 'year');
27+
result = format(date, 'year');
1828
} else if (+duration >= +new Duration({ duration: { days: 28 } })) {
1929
// Month
2030
const isFirst = i === 0 || +timeYear.floor(date) === +date;
2131
if (multiline) {
22-
return (
23-
format(date, 'month', { variant: 'short' }) + (isFirst ? `\n${format(date, 'year')}` : '')
24-
);
32+
result = [format(date, 'month', { variant: 'short' }), isFirst && format(date, 'year')];
2533
} else {
26-
return (
34+
result =
2735
format(date, 'month', { variant: 'short' }) +
28-
(isFirst ? ` '${format(date, 'year', { variant: 'short' })}` : '')
29-
);
36+
(isFirst ? ` '${format(date, 'year', { variant: 'short' })}` : '');
3037
}
3138
} else if (+duration >= +new Duration({ duration: { days: 1 } })) {
3239
// Day
3340
const isFirst = i === 0 || date.getDate() <= duration.days;
3441
if (multiline) {
35-
return (
36-
format(date, 'custom', { custom: DateToken.DayOfMonth_numeric }) +
37-
(isFirst ? `\n${format(date, 'month', { variant: 'short' })}` : '')
38-
);
42+
result = [
43+
format(date, 'custom', { custom: DateToken.DayOfMonth_numeric }),
44+
isFirst && format(date, 'month', { variant: 'short' }),
45+
];
3946
} else {
40-
return format(date, 'day', { variant: 'short' });
47+
result = format(date, 'day', { variant: 'short' });
4148
}
4249
} else if (+duration >= +new Duration({ duration: { hours: 1 } })) {
4350
// Hours
4451
const isFirst = i === 0 || +timeDay.floor(date) === +date;
4552
if (multiline) {
46-
return (
47-
format(date, 'custom', { custom: DateToken.Hour_numeric }) +
48-
(isFirst ? `\n${format(date, 'day', { variant: 'short' })}` : '')
49-
);
53+
result = [
54+
format(date, 'custom', { custom: DateToken.Hour_numeric }),
55+
isFirst && format(date, 'day', { variant: 'short' }),
56+
];
5057
} else {
51-
return isFirst
58+
result = isFirst
5259
? format(date, 'day', { variant: 'short' })
5360
: format(date, 'custom', { custom: DateToken.Hour_numeric });
5461
}
5562
} else if (+duration >= +new Duration({ duration: { minutes: 1 } })) {
5663
// Minutes
5764
const isFirst = i === 0 || +timeDay.floor(date) === +date;
5865
if (multiline) {
59-
return (
60-
format(date, 'time', { variant: 'short' }) +
61-
(isFirst ? `\n${format(date, 'day', { variant: 'short' })}` : '')
62-
);
66+
result = [
67+
format(date, 'time', { variant: 'short' }),
68+
isFirst && format(date, 'day', { variant: 'short' }),
69+
];
6370
} else {
64-
return format(date, 'time', { variant: 'short' });
71+
result = format(date, 'time', { variant: 'short' });
6572
}
6673
} else if (+duration >= +new Duration({ duration: { seconds: 1 } })) {
6774
// Seconds
6875
const isFirst = i === 0 || +timeDay.floor(date) === +date;
69-
return (
70-
format(date, 'time') +
71-
(multiline && isFirst ? `\n${format(date, 'day', { variant: 'short' })}` : '')
72-
);
76+
result = [
77+
format(date, 'time'),
78+
multiline && isFirst && format(date, 'day', { variant: 'short' }),
79+
];
7380
} else if (+duration >= +new Duration({ duration: { milliseconds: 1 } })) {
7481
// Milliseconds
7582
const isFirst = i === 0 || +timeDay.floor(date) === +date;
76-
return (
83+
result = [
7784
format(date, 'custom', {
7885
custom: [
7986
DateToken.Hour_2Digit,
@@ -82,10 +89,28 @@ export function getDurationFormat(duration: Duration, multiline = false) {
8289
DateToken.MiliSecond_3,
8390
DateToken.Hour_woAMPM,
8491
],
85-
}) + (multiline && isFirst ? `\n${format(date, 'day', { variant: 'short' })}` : '')
86-
);
92+
}),
93+
multiline && isFirst && format(date, 'day', { variant: 'short' }),
94+
];
95+
} else {
96+
result = date.toString();
97+
}
98+
99+
if (Array.isArray(result)) {
100+
switch (placement) {
101+
case 'top':
102+
return result.filter(Boolean).reverse().join('\n');
103+
case 'bottom':
104+
return result.filter(Boolean).join('\n');
105+
case 'left':
106+
return result.filter(Boolean).reverse().join(' ');
107+
case 'right':
108+
return result.filter(Boolean).join(' ');
109+
default:
110+
return result.filter(Boolean).join('\n');
111+
}
87112
} else {
88-
return date.toString();
113+
return result;
89114
}
90115
};
91116
}
@@ -127,13 +152,16 @@ export function resolveTickVals(scale: AnyScale, ticks?: TicksConfig, count?: nu
127152
return [];
128153
}
129154

130-
export function resolveTickFormat(
131-
scale: AnyScale,
132-
ticks?: TicksConfig,
133-
count?: number,
134-
formatType?: FormatType | FormatConfig,
135-
multiline = false
136-
) {
155+
export function resolveTickFormat(options: {
156+
scale: AnyScale;
157+
ticks?: TicksConfig;
158+
count?: number;
159+
formatType?: FormatType | FormatConfig;
160+
multiline?: boolean;
161+
placement?: AxisProps['placement'];
162+
}) {
163+
const { scale, ticks, count, formatType, multiline, placement } = options;
164+
137165
// Explicit format
138166
if (formatType) {
139167
// @ts-expect-error - improve types
@@ -145,11 +173,11 @@ export function resolveTickFormat(
145173
if (isLiteralObject(ticks) && 'interval' in ticks && ticks.interval != null) {
146174
const start = ticks.interval.floor(new Date());
147175
const end = ticks.interval.ceil(new Date());
148-
return getDurationFormat(new Duration({ start, end }), multiline);
176+
return getDurationFormat(new Duration({ start, end }), { multiline, placement });
149177
} else {
150178
// Compare first 2 ticks to determine duration between ticks for formatting
151179
const [start, end] = timeTicks(scale.domain()[0], scale.domain()[1], count);
152-
return getDurationFormat(new Duration({ start, end }), multiline);
180+
return getDurationFormat(new Duration({ start, end }), { multiline, placement });
153181
}
154182
}
155183

packages/layerchart/src/routes/docs/components/Axis/+page.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,7 @@
716716
padding={{ top: 20, bottom: 20, left: 20, right: 20 }}
717717
>
718718
<Layer type={shared.renderContext}>
719+
<Axis placement="top" rule grid tickMultiline {tickSpacing} />
719720
<Axis placement="bottom" rule grid tickMultiline {tickSpacing} />
720721
</Layer>
721722
</Chart>

0 commit comments

Comments
 (0)