Skip to content

Commit 85aa885

Browse files
committed
feat: add text.lineHeight + support css variable font sizes
1 parent 2df417d commit 85aa885

File tree

78 files changed

+214
-105
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+214
-105
lines changed

src/lib/helpers/getBaseStyles.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,13 @@ export function maybeToPixel(cssKey: string, value: string | number) {
5050
}
5151
return value;
5252
}
53+
54+
export function maybeFromPixel(value: string | number) {
55+
return typeof value === 'string' && value.endsWith('px') ? +value.slice(0, -2) : value;
56+
}
57+
58+
export function maybeFromRem(value: string | number, rootFontSize: number = 16) {
59+
return typeof value === 'string' && value.endsWith('rem')
60+
? +value.slice(0, -3) * rootFontSize
61+
: value;
62+
}

src/lib/helpers/resolve.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export function resolveProp<T>(
3838
) {
3939
return datum[accessor] as T;
4040
}
41+
4142
return isRawValue(accessor) ? accessor : _defaultValue;
4243
}
4344

src/lib/marks/Text.svelte

Lines changed: 10 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
* the line anchor for vertical position; top, bottom, or middle
1919
*/
2020
lineAnchor?: ConstantAccessor<'bottom' | 'top' | 'middle'>;
21+
/**
22+
* line height as multiplier of font size
23+
* @default 1.2
24+
*/
25+
lineHeight?: ConstantAccessor<number>;
2126
frameAnchor?: ConstantAccessor<
2227
| 'bottom'
2328
| 'top'
@@ -35,7 +40,6 @@
3540
import { getContext, type Snippet } from 'svelte';
3641
import GroupMultiple from './helpers/GroupMultiple.svelte';
3742
import type {
38-
PlotContext,
3943
DataRecord,
4044
BaseMarkProps,
4145
ConstantAccessor,
@@ -46,11 +50,14 @@
4650
import Mark from '../Mark.svelte';
4751
import { sort } from '$lib/index.js';
4852
53+
import MultilineText from './helpers/MultilineText.svelte';
54+
4955
const DEFAULTS = {
5056
fontSize: 12,
5157
fontWeight: 500,
5258
strokeWidth: 1.6,
5359
frameAnchor: 'center',
60+
lineHeight: 1.1,
5461
...getContext<PlotDefaults>('svelteplot/_defaults').text
5562
};
5663
@@ -65,21 +72,12 @@
6572
...markProps
6673
});
6774
68-
const { getPlotState } = getContext<PlotContext>('svelteplot');
69-
let plot = $derived(getPlotState());
70-
71-
const LINE_ANCHOR = {
72-
bottom: 'auto',
73-
middle: 'central',
74-
top: 'hanging'
75-
} as const;
76-
7775
const args = $derived(
7876
sort({
7977
data,
8078
...options
8179
})
82-
);
80+
) as TextMarkProps;
8381
</script>
8482

8583
<Mark
@@ -101,99 +99,9 @@
10199
<GroupMultiple class="text {className}" length={className ? 2 : args.data.length}>
102100
{#each scaledData as d, i (i)}
103101
{#if d.valid}
104-
{@const title = resolveProp(args.title, d.datum, '')}
105-
{@const frameAnchor = resolveProp(args.frameAnchor, d.datum)}
106-
{@const isLeft =
107-
frameAnchor === 'left' ||
108-
frameAnchor === 'top-left' ||
109-
frameAnchor === 'bottom-left'}
110-
{@const isRight =
111-
frameAnchor === 'right' ||
112-
frameAnchor === 'top-right' ||
113-
frameAnchor === 'bottom-right'}
114-
{@const isTop =
115-
frameAnchor === 'top' ||
116-
frameAnchor === 'top-left' ||
117-
frameAnchor === 'top-right'}
118-
{@const isBottom =
119-
frameAnchor === 'bottom' ||
120-
frameAnchor === 'bottom-left' ||
121-
frameAnchor === 'bottom-right'}
122-
{@const [x, y] =
123-
args.x != null && args.y != null
124-
? [d.x, d.y]
125-
: [
126-
args.x != null
127-
? d.x
128-
: isLeft
129-
? plot.options.marginLeft
130-
: isRight
131-
? plot.options.marginLeft + plot.facetWidth
132-
: plot.options.marginLeft + plot.facetWidth * 0.5,
133-
args.y != null
134-
? d.y
135-
: isTop
136-
? plot.options.marginTop
137-
: isBottom
138-
? plot.options.marginTop + plot.facetHeight
139-
: plot.options.marginTop + plot.facetHeight * 0.5
140-
]}
141-
142-
{@const dx = +resolveProp(args.dx, d.datum, 0)}
143-
{@const dy = +resolveProp(args.dy, d.datum, 0)}
144102
{@const textLines = String(resolveProp(args.text, d.datum, '')).split('\n')}
145-
{@const lineAnchor = resolveProp(
146-
args.lineAnchor,
147-
d.datum,
148-
args.y != null ? 'middle' : isTop ? 'top' : isBottom ? 'bottom' : 'middle'
149-
)}
150-
{@const textClassName = resolveProp(args.textClass, d.datum, null)}
151-
152-
{@const [style, styleClass] = resolveStyles(
153-
plot,
154-
{ ...d, __tspanIndex: 0 },
155-
{
156-
fontSize: 12,
157-
fontWeight: 500,
158-
strokeWidth: 1.6,
159-
textAnchor: isLeft ? 'start' : isRight ? 'end' : 'middle',
160-
...args
161-
},
162-
'fill',
163-
usedScales
164-
)}
165103

166-
{#if textLines.length > 1}
167-
<!-- multiline text-->
168-
{@const fontSize = resolveProp(args.fontSize, d.datum) || 12}
169-
<text
170-
class={[textClassName]}
171-
dominant-baseline={LINE_ANCHOR[lineAnchor]}
172-
transform="translate({Math.round(x + dx)},{Math.round(
173-
y +
174-
dy -
175-
(lineAnchor === 'bottom'
176-
? textLines.length - 1
177-
: lineAnchor === 'middle'
178-
? (textLines.length - 1) * 0.5
179-
: 0) *
180-
fontSize
181-
)})"
182-
>{#each textLines as line, l (l)}<tspan
183-
x="0"
184-
dy={l ? fontSize : 0}
185-
class={styleClass}
186-
{style}>{line}</tspan
187-
>{/each}{#if title}<title>{title}</title>{/if}</text>
188-
{:else}
189-
<!-- singleline text-->
190-
<text
191-
class={[textClassName, styleClass]}
192-
dominant-baseline={LINE_ANCHOR[lineAnchor]}
193-
transform="translate({Math.round(x + dx)},{Math.round(y + dy)})"
194-
{style}
195-
>{textLines[0]}{#if title}<title>{title}</title>{/if}</text>
196-
{/if}
104+
<MultilineText {textLines} {d} {args} {usedScales} />
197105
{/if}
198106
{/each}
199107
</GroupMultiple>
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<script lang="ts">
2+
import { resolveProp, resolveStyles } from 'svelteplot/helpers/resolve';
3+
import { getContext, type ComponentProps } from 'svelte';
4+
import type { PlotContext, ScaledDataRecord, UsedScales } from 'svelteplot/types';
5+
import type Text from '../Text.svelte';
6+
import { CSS_VAR } from 'svelteplot/constants';
7+
import { maybeFromPixel, maybeFromRem } from 'svelteplot/helpers/getBaseStyles';
8+
9+
const LINE_ANCHOR = {
10+
bottom: 'auto',
11+
middle: 'central',
12+
top: 'hanging'
13+
} as const;
14+
15+
const { getPlotState } = getContext<PlotContext>('svelteplot');
16+
const plot = $derived(getPlotState());
17+
18+
let {
19+
textLines,
20+
d,
21+
args,
22+
usedScales
23+
}: {
24+
textLines: string[];
25+
d: ScaledDataRecord;
26+
args: ComponentProps<typeof Text>;
27+
usedScales: UsedScales;
28+
} = $props();
29+
30+
const title = $derived(resolveProp(args.title, d.datum, ''));
31+
const frameAnchor = $derived(resolveProp(args.frameAnchor, d.datum));
32+
const isLeft = $derived(
33+
frameAnchor === 'left' || frameAnchor === 'top-left' || frameAnchor === 'bottom-left'
34+
);
35+
const isRight = $derived(
36+
frameAnchor === 'right' || frameAnchor === 'top-right' || frameAnchor === 'bottom-right'
37+
);
38+
const isTop = $derived(
39+
frameAnchor === 'top' || frameAnchor === 'top-left' || frameAnchor === 'top-right'
40+
);
41+
const isBottom = $derived(
42+
frameAnchor === 'bottom' || frameAnchor === 'bottom-left' || frameAnchor === 'bottom-right'
43+
);
44+
const lineAnchor = $derived(
45+
resolveProp(
46+
args.lineAnchor,
47+
d.datum,
48+
args.y != null ? 'middle' : isTop ? 'top' : isBottom ? 'bottom' : 'middle'
49+
)
50+
);
51+
const textClassName = $derived(resolveProp(args.textClass, d.datum, null));
52+
const [x, y] = $derived(
53+
args.x != null && args.y != null
54+
? [d.x, d.y]
55+
: [
56+
args.x != null
57+
? d.x
58+
: isLeft
59+
? plot.options.marginLeft
60+
: isRight
61+
? plot.options.marginLeft + plot.facetWidth
62+
: plot.options.marginLeft + plot.facetWidth * 0.5,
63+
args.y != null
64+
? d.y
65+
: isTop
66+
? plot.options.marginTop
67+
: isBottom
68+
? plot.options.marginTop + plot.facetHeight
69+
: plot.options.marginTop + plot.facetHeight * 0.5
70+
]
71+
);
72+
73+
const dx = $derived(+resolveProp(args.dx, d.datum, 0));
74+
const dy = $derived(+resolveProp(args.dy, d.datum, 0));
75+
76+
const [style, styleClass] = $derived(
77+
resolveStyles(
78+
plot,
79+
{ ...d, __tspanIndex: 0 },
80+
{
81+
fontSize: 12,
82+
fontWeight: 500,
83+
strokeWidth: 1.6,
84+
textAnchor: isLeft ? 'start' : isRight ? 'end' : 'middle',
85+
...args
86+
},
87+
'fill',
88+
usedScales
89+
)
90+
);
91+
92+
const fontSize = $derived(
93+
textLines.length > 1 ? (resolveProp(args.fontSize, d.datum) ?? 12) : 0
94+
);
95+
let textElement: SVGTextElement | null = $state(null);
96+
97+
const rootFontSize = $derived(
98+
textElement?.ownerDocument?.documentElement && textLines.length > 1
99+
? maybeFromPixel(getComputedStyle(textElement.ownerDocument.documentElement).fontSize)
100+
: 14
101+
);
102+
103+
const computedFontSize = $derived(
104+
textElement && textLines.length > 1 && CSS_VAR.test(fontSize)
105+
? maybeFromRem(
106+
maybeFromPixel(
107+
getComputedStyle(textElement).getPropertyValue(
108+
`--${fontSize.match(CSS_VAR)[1]}`
109+
)
110+
),
111+
rootFontSize
112+
)
113+
: fontSize
114+
);
115+
116+
const lineHeight = $derived(
117+
textLines.length > 1 ? (resolveProp(args.lineHeight, d.datum) ?? 1.2) : 0
118+
);
119+
</script>
120+
121+
{#if textLines.length > 1}
122+
<!-- multiline text-->
123+
<text
124+
bind:this={textElement}
125+
class={[textClassName]}
126+
dominant-baseline={LINE_ANCHOR[lineAnchor]}
127+
transform="translate({Math.round(x + dx)},{Math.round(
128+
y +
129+
dy -
130+
(lineAnchor === 'bottom'
131+
? textLines.length - 1
132+
: lineAnchor === 'middle'
133+
? (textLines.length - 1) * 0.5
134+
: 0) *
135+
computedFontSize *
136+
lineHeight
137+
)})"
138+
>{#each textLines as line, l (l)}<tspan
139+
x="0"
140+
dy={l ? computedFontSize * lineHeight : 0}
141+
class={styleClass}
142+
{style}>{line}</tspan
143+
>{/each}{#if title}<title>{title}</title>{/if}</text>
144+
{:else}
145+
<!-- singleline text-->
146+
<text
147+
class={[textClassName, styleClass]}
148+
dominant-baseline={LINE_ANCHOR[lineAnchor]}
149+
transform="translate({Math.round(x + dx)},{Math.round(y + dy)})"
150+
{style}
151+
>{textLines[0]}{#if title}<title>{title}</title>{/if}</text>
152+
{/if}
153+
154+
<style>
155+
text {
156+
paint-order: stroke fill;
157+
}
158+
</style>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<script lang="ts" module>
2+
export const sortKey = 99;
3+
export const title = 'CSS variable font size';
4+
export const description =
5+
'The font size and line height are being used to position multiline text in SVG. You can also use CSS variables to set the font size (currently only in <tt>px</tt> and <tt>rem</tt>).';
6+
</script>
7+
8+
<script lang="ts">
9+
import { Plot, Text } from 'svelteplot';
10+
import Frame from 'svelteplot/marks/Frame.svelte';
11+
</script>
12+
13+
<Plot axes={false}>
14+
<Frame borderRadius={20} strokeOpacity={0.25} />
15+
<Text
16+
fontSize="var(--my-font-size)"
17+
fill="var(--svp-accent)"
18+
fontWeight="bold"
19+
text={`SveltePlot\nis great`} />
20+
</Plot>
21+
22+
<style>
23+
:root {
24+
--my-font-size: 3rem;
25+
@media screen and (max-width: 600px) {
26+
--my-font-size: 27px;
27+
}
28+
}
29+
</style>

src/routes/marks/text/+page.md

Lines changed: 6 additions & 3 deletions

static/examples/arrow/metro.dark.png

-495 Bytes

static/examples/arrow/metro.png

-458 Bytes
-23 Bytes
-22 Bytes

0 commit comments

Comments
 (0)
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