Skip to content

Commit 5f00c3f

Browse files
authored
Merge pull request #107 from techniq/radar-example
Radial Axis/Spline/Area/Point support
2 parents 9e25db5 + 70e032c commit 5f00c3f

13 files changed

Lines changed: 38773 additions & 22 deletions

File tree

.changeset/modern-pets-jam.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+
Support radial / polar coordinate system (along with cartesian) for Axis, Spline, Area, and Point

.changeset/tiny-cooks-wash.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+
[Axis] Fallback to tick value if no tick format defined (band scales)

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script lang="ts">
22
import { getContext, type ComponentProps } from 'svelte';
33
import type { tweened as tweenedStore } from 'svelte/motion';
4-
import { type Area, area as d3Area } from 'd3-shape';
4+
import { type Area, area as d3Area, areaRadial } from 'd3-shape';
55
import type { CurveFactory } from 'd3-shape';
66
import { max } from 'd3-array';
77
import { interpolatePath } from 'd3-interpolate-path';
@@ -20,6 +20,9 @@
2020
/** Pass `<path d={...} />` explicitly instead of calculating from data / context */
2121
export let pathData: string | undefined | null = undefined;
2222
23+
/** Use radial instead of cartesian area generator, mapping `x` to `angle` and `y0`/`y1 to `innerRadius`/`outerRadius. Radial lines are positioned relative to the origin, use transform (ex. `<Group center>`) to change the origin */
24+
export let radial = false;
25+
2326
/** Override x accessor */
2427
export let x: any = undefined; // TODO: Update Type
2528
@@ -42,10 +45,15 @@
4245
$: tweenedOptions = tweened ? { interpolate: interpolatePath, ...tweened } : false;
4346
$: tweened_d = motionStore('', { tweened: tweenedOptions });
4447
$: {
45-
const path = d3Area()
46-
.x(x ?? $xGet)
47-
.y0(y0 ?? max($yRange))
48-
.y1(y1 ?? $yGet);
48+
const path = radial
49+
? areaRadial()
50+
.angle(x ?? $xGet)
51+
.innerRadius(y0 ?? max($yRange))
52+
.outerRadius(y1 ?? $yGet)
53+
: d3Area()
54+
.x(x ?? $xGet)
55+
.y0(y0 ?? max($yRange))
56+
.y1(y1 ?? $yGet);
4957
if (curve) path.curve(curve);
5058
if (defined) path.defined(defined);
5159

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

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
import type { SVGAttributes } from 'svelte/elements';
44
import { format as formatValue, type FormatType, cls } from 'svelte-ux';
55
import { extent } from 'd3-array';
6+
import { pointRadial } from 'd3-shape';
67
78
import Text from './Text.svelte';
89
import { isScaleBand } from '$lib/utils/scales';
910
1011
const { xScale, yScale, xRange, yRange, width } = getContext('LayerCake');
1112
1213
/** Location of axis */
13-
export let placement: 'top' | 'bottom' | 'left' | 'right';
14+
export let placement: 'top' | 'bottom' | 'left' | 'right' | 'angle' | 'radius';
1415
1516
/** Draw a rule line. Use Rule component for greater rendering order control */
1617
export let rule: boolean | SVGAttributes<SVGLineElement> = false;
@@ -27,8 +28,15 @@
2728
export let format: FormatType = undefined;
2829
export let labelProps: Partial<ComponentProps<Text>> | undefined = undefined;
2930
30-
$: orientation = ['top', 'bottom'].includes(placement) ? 'horizontal' : 'vertical';
31-
$: scale = orientation === 'horizontal' ? $xScale : $yScale;
31+
$: orientation =
32+
placement === 'angle'
33+
? 'angle'
34+
: placement === 'radius'
35+
? 'radius'
36+
: ['top', 'bottom'].includes(placement)
37+
? 'horizontal'
38+
: 'vertical';
39+
$: scale = ['horizontal', 'angle'].includes(orientation) ? $xScale : $yScale;
3240
3341
$: [xRangeMin, xRangeMax] = extent($xRange);
3442
$: [yRangeMin, yRangeMax] = extent($yRange);
@@ -64,10 +72,22 @@
6472
x: xRangeMax,
6573
y: $yScale(tick) + (isScaleBand($yScale) ? $yScale.bandwidth() / 2 : 0),
6674
};
75+
76+
case 'angle':
77+
return {
78+
x: $xScale(tick),
79+
y: yRangeMax,
80+
};
81+
82+
case 'radius':
83+
return {
84+
x: xRangeMin,
85+
y: $yScale(tick),
86+
};
6787
}
6888
}
6989
70-
function getDefaultLabelProps(): ComponentProps<Text> {
90+
function getDefaultLabelProps(tick: any): ComponentProps<Text> {
7191
switch (placement) {
7292
case 'top':
7393
return {
@@ -98,6 +118,24 @@
98118
dx: 4,
99119
dy: -2, // manually adjusted until Text supports custom styles
100120
};
121+
122+
case 'angle':
123+
const xValue = $xScale(tick);
124+
return {
125+
textAnchor:
126+
xValue === 0 || xValue === Math.PI ? 'middle' : xValue > Math.PI ? 'end' : 'start',
127+
verticalAnchor: 'middle',
128+
dx: 0,
129+
dy: -2, // manually adjusted until Text supports custom styles
130+
};
131+
132+
case 'radius':
133+
return {
134+
textAnchor: 'middle',
135+
verticalAnchor: 'middle',
136+
dx: 2,
137+
dy: -2, // manually adjusted until Text supports custom styles
138+
};
101139
}
102140
}
103141
</script>
@@ -126,10 +164,22 @@
126164
class={cls('rule stroke-surface-content/50', lineProps?.class)}
127165
/>
128166
{/if}
167+
168+
<!-- TODO: angle rule? -->
169+
170+
{#if orientation === 'radius'}
171+
<circle
172+
r={$yRange[0] || 0}
173+
{...lineProps}
174+
class={cls('rule stroke-surface-content/20 fill-none', lineProps?.class)}
175+
/>
176+
{/if}
129177
{/if}
130178

131179
{#each tickVals as tick, i}
132180
{@const tickCoords = getCoords(tick)}
181+
{@const radialTickCoords = pointRadial(tickCoords.x, tickCoords.y)}
182+
133183
<g>
134184
{#if grid !== false}
135185
{@const lineProps = typeof grid === 'object' ? grid : null}
@@ -151,6 +201,24 @@
151201
{...lineProps}
152202
class={cls('grid stroke-surface-content/10', lineProps?.class)}
153203
/>
204+
{:else if orientation === 'angle'}
205+
{@const [x1, y1] = pointRadial(tickCoords.x, yRangeMin)}
206+
{@const [x2, y2] = pointRadial(tickCoords.x, yRangeMax)}
207+
208+
<line
209+
{x1}
210+
{y1}
211+
{x2}
212+
{y2}
213+
{...lineProps}
214+
class={cls('test grid stroke-surface-content/10', lineProps?.class)}
215+
/>
216+
{:else if orientation === 'radius'}
217+
<circle
218+
r={tickCoords.y}
219+
{...lineProps}
220+
class={cls('grid stroke-surface-content/10 fill-none', lineProps?.class)}
221+
/>
154222
{/if}
155223
{/if}
156224

@@ -174,10 +242,10 @@
174242
{/if}
175243

176244
<Text
177-
x={tickCoords.x}
178-
y={tickCoords.y}
179-
value={formatValue(tick, format ?? scale.tickFormat?.())}
180-
{...getDefaultLabelProps()}
245+
x={orientation === 'angle' ? radialTickCoords[0] : tickCoords.x}
246+
y={orientation === 'angle' ? radialTickCoords[1] : tickCoords.y}
247+
value={formatValue(tick, format ?? scale.tickFormat?.() ?? ((v) => v))}
248+
{...getDefaultLabelProps(tick)}
181249
{...labelProps}
182250
class={cls(
183251
'label text-[10px] stroke-surface-100 [stroke-width:2px] font-light',

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,13 @@
9999
let:width
100100
let:element
101101
let:xScale
102+
let:xGet
102103
let:yScale
104+
let:yGet
103105
let:zScale
106+
let:zGet
104107
let:rScale
108+
let:rGet
105109
let:padding
106110
let:data
107111
let:flatData
@@ -120,9 +124,13 @@
120124
{projection}
121125
{tooltip}
122126
{xScale}
127+
{xGet}
123128
{yScale}
129+
{yGet}
124130
{zScale}
131+
{zGet}
125132
{rScale}
133+
{rGet}
126134
{padding}
127135
{data}
128136
{flatData}
@@ -138,9 +146,13 @@
138146
{element}
139147
{projection}
140148
{xScale}
149+
{xGet}
141150
{yScale}
151+
{yGet}
142152
{zScale}
153+
{zGet}
143154
{rScale}
155+
{rGet}
144156
{padding}
145157
{data}
146158
{flatData}

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import Circle from './Circle.svelte';
77
import Link from './Link.svelte';
88
import { isScaleBand } from '../utils/scales';
9+
import { pointRadial } from 'd3-shape';
910
1011
const context = getContext('LayerCake') as any;
1112
const { data, xGet, y, yGet, xScale, yScale, rGet, config } = context;
@@ -16,6 +17,9 @@
1617
export let offsetX: Offset = undefined;
1718
export let offsetY: Offset = undefined;
1819
20+
/** Use radial instead of cartesian line generator, mapping `x` to `angle` and `y` to `radius`. Radial points are positioned relative to the origin, use transform (ex. `<Group center>`) to change the origin */
21+
export let radial = false;
22+
1923
/** Enable showing links between related points (array x/y accessors) */
2024
export let links: boolean | Partial<ComponentProps<Link>> = false;
2125
@@ -24,7 +28,7 @@
2428
return offset(value, context);
2529
} else if (offset != null) {
2630
return offset;
27-
} else if (isScaleBand(scale)) {
31+
} else if (isScaleBand(scale) && !radial) {
2832
return scale.bandwidth() / 2;
2933
} else {
3034
return 0;
@@ -127,9 +131,10 @@
127131

128132
<g class="point-group">
129133
{#each points as point}
134+
{@const radialPoint = pointRadial(point.x, point.y)}
130135
<Circle
131-
cx={point.x}
132-
cy={point.y}
136+
cx={radial ? radialPoint[0] : point.x}
137+
cy={radial ? radialPoint[1] : point.y}
133138
{r}
134139
fill={$config.r ? $rGet(point.data) : null}
135140
{...$$restProps}

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import { draw as _drawTransition } from 'svelte/transition';
66
import { cubicInOut } from 'svelte/easing';
77
8-
import { line as d3Line } from 'd3-shape';
8+
import { line as d3Line, lineRadial } from 'd3-shape';
99
import type { CurveFactory, CurveFactoryLineOnly, Line } from 'd3-shape';
1010
// import { interpolateString } from 'd3-interpolate';
1111
import { interpolatePath } from 'd3-interpolate-path';
@@ -22,9 +22,12 @@
2222
/** Pass `<path d={...} />` explicitly instead of calculating from data / context */
2323
export let pathData: string | undefined | null = undefined;
2424
25-
/** Override x accessor */
25+
/** Use radial instead of cartesian line generator, mapping `x` to `angle` and `y` to `radius`. Radial lines are positioned relative to the origin, use transform (ex. `<Group center>`) to change the origin */
26+
export let radial = false;
27+
28+
/** Override `x` accessor from Chart context. Applies to `angle` when `radial=true` */
2629
export let x: any = undefined; // TODO: Update Type
27-
/** Override y accessor */
30+
/** Override `y` accessor from Chart context. Applies to `radius` when `radial=true` */
2831
export let y: any = undefined; // TODO: Update Type
2932
3033
/** Interpolate path data using d3-interpolate-path. Works best without `draw` enabled */
@@ -48,9 +51,13 @@
4851
$: tweenedOptions = tweened ? { interpolate: interpolatePath, ...tweened } : false;
4952
$: tweened_d = motionStore('', { tweened: tweenedOptions });
5053
$: {
51-
const path = d3Line()
52-
.x(x ?? $xGet)
53-
.y(y ?? $yGet);
54+
const path = radial
55+
? lineRadial()
56+
.angle(x ?? $xGet)
57+
.radius(y ?? $yGet)
58+
: d3Line()
59+
.x(x ?? $xGet)
60+
.y(y ?? $yGet);
5461
if (curve) path.curve(curve);
5562
if (defined) path.defined(defined);
5663

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,36 @@ export function degreesToRadians(degrees: number) {
1111
export function radiansToDegrees(radians: number) {
1212
return radians * (180 / Math.PI);
1313
}
14+
15+
/**
16+
* Convert polar to cartesian coordinate system.
17+
* see also: https://d3js.org/d3-shape/symbol#pointRadial
18+
* @param angle - Angle in radians
19+
* @param radius - Radius
20+
*/
21+
export function polarToCartesian(angle: number, radius: number) {
22+
return {
23+
x: radius * Math.cos(angle),
24+
y: radius * Math.sin(angle),
25+
};
26+
}
27+
28+
/**
29+
* Convert cartesian to polar coordinate system. Angle in radians
30+
*/
31+
export function cartesianToPolar(x: number, y: number) {
32+
return {
33+
radius: Math.sqrt(x ** 2 + y ** 2),
34+
angle: Math.atan(y / x),
35+
};
36+
}
37+
38+
/** Convert celsius temperature to fahrenheit */
39+
export function celsiusToFahrenheit(temperature: number) {
40+
return temperature * (9 / 5) + 32;
41+
}
42+
43+
/** Convert fahrenheit temperature to celsius */
44+
export function fahrenheitToCelsius(temperature: number) {
45+
return (temperature - 32) * (5 / 9);
46+
}

packages/layerchart/src/routes/_NavMenu.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
'Line',
1818
'Oscilloscope',
1919
'PunchCard',
20+
'RadialLine',
2021
'Scatter',
2122
'Sparkbar',
2223
'Sparkline',

0 commit comments

Comments
 (0)