diff --git a/.changeset/nine-pens-design.md b/.changeset/nine-pens-design.md
new file mode 100644
index 000000000..b37e68316
--- /dev/null
+++ b/.changeset/nine-pens-design.md
@@ -0,0 +1,5 @@
+---
+'layerchart': patch
+---
+
+feat: Add Polygon primitive
diff --git a/.changeset/wide-berries-invite.md b/.changeset/wide-berries-invite.md
new file mode 100644
index 000000000..396e96a1a
--- /dev/null
+++ b/.changeset/wide-berries-invite.md
@@ -0,0 +1,5 @@
+---
+'layerchart': patch
+---
+
+feat: Add Ellipse primitive
diff --git a/packages/layerchart/src/lib/components/Ellipse.svelte b/packages/layerchart/src/lib/components/Ellipse.svelte
new file mode 100644
index 000000000..3bde17ec2
--- /dev/null
+++ b/packages/layerchart/src/lib/components/Ellipse.svelte
@@ -0,0 +1,187 @@
+
+
+
+
+{#if renderCtx === 'svg'}
+
+{/if}
diff --git a/packages/layerchart/src/lib/components/Polygon.svelte b/packages/layerchart/src/lib/components/Polygon.svelte
new file mode 100644
index 000000000..fad2ba4dd
--- /dev/null
+++ b/packages/layerchart/src/lib/components/Polygon.svelte
@@ -0,0 +1,283 @@
+
+
+
+
+{#if renderCtx === 'svg'}
+
+{/if}
diff --git a/packages/layerchart/src/lib/components/index.ts b/packages/layerchart/src/lib/components/index.ts
index a09017b2e..1e4cfa35a 100644
--- a/packages/layerchart/src/lib/components/index.ts
+++ b/packages/layerchart/src/lib/components/index.ts
@@ -43,6 +43,8 @@ export { default as Connector } from './Connector.svelte';
export * from './Connector.svelte';
export { default as Dagre } from './Dagre.svelte';
export * from './Dagre.svelte';
+export { default as Ellipse } from './Ellipse.svelte';
+export * from './Ellipse.svelte';
export { default as Frame } from './Frame.svelte';
export * from './Frame.svelte';
export { default as ForceSimulation } from './ForceSimulation.svelte';
@@ -103,6 +105,8 @@ export { default as Point } from './Point.svelte';
export * from './Point.svelte';
export { default as Points } from './Points.svelte';
export * from './Points.svelte';
+export { default as Polygon } from './Polygon.svelte';
+export * from './Polygon.svelte';
export { default as RadialGradient } from './RadialGradient.svelte';
export * from './RadialGradient.svelte';
export { default as Rect } from './Rect.svelte';
diff --git a/packages/layerchart/src/lib/utils/canvas.ts b/packages/layerchart/src/lib/utils/canvas.ts
index 7ee44231f..329dbac31 100644
--- a/packages/layerchart/src/lib/utils/canvas.ts
+++ b/packages/layerchart/src/lib/utils/canvas.ts
@@ -232,6 +232,28 @@ export function renderCircle(
ctx.closePath();
}
+export function renderEllipse(
+ ctx: CanvasRenderingContext2D,
+ coords: { cx: number; cy: number; rx: number; ry: number },
+ styleOptions: ComputedStylesOptions = {}
+) {
+ ctx.beginPath();
+ ctx.ellipse(coords.cx, coords.cy, coords.rx, coords.ry, 0, 0, 2 * Math.PI);
+ render(
+ ctx,
+ {
+ fill: (ctx) => {
+ ctx.fill();
+ },
+ stroke: (ctx) => {
+ ctx.stroke();
+ },
+ },
+ styleOptions
+ );
+ ctx.closePath();
+}
+
/** Clear canvas accounting for Canvas `context.translate(...)` */
export function clearCanvasContext(
ctx: CanvasRenderingContext2D,
diff --git a/packages/layerchart/src/lib/utils/path.ts b/packages/layerchart/src/lib/utils/path.ts
index 98305a127..f8a4344c4 100644
--- a/packages/layerchart/src/lib/utils/path.ts
+++ b/packages/layerchart/src/lib/utils/path.ts
@@ -58,6 +58,36 @@ export function spikePath({
return pathData;
}
+/** Create rounded polygon path
+ *
+ * @param coords - Array of points (x, y)
+ * @param radius - Radius of the curve
+ * @returns String of path data
+ */
+export function roundedPolygonPath(coords: { x: number; y: number }[], radius: number) {
+ if (radius === 0) {
+ // Simple polygon with straight lines
+ return `M${coords[0].x},${coords[0].y}${coords
+ .slice(1)
+ .map((p) => `L${p.x},${p.y}`)
+ .join('')}Z`;
+ }
+
+ let path = '';
+ const length = coords.length + 1;
+ for (let i = 0; i < length; i++) {
+ const a = coords[i % coords.length];
+ const b = coords[(i + 1) % coords.length];
+ const t = Math.min(radius / Math.hypot(b.x - a.x, b.y - a.y), 0.5);
+
+ if (i == 0) path += `M${a.x * (1 - t) + b.x * t},${a.y * (1 - t) + b.y * t}`;
+ if (i > 0) path += `Q${a.x},${a.y} ${a.x * (1 - t) + b.x * t},${a.y * (1 - t) + b.y * t}`;
+ if (i < length - 1) path += `L${a.x * t + b.x * (1 - t)},${a.y * t + b.y * (1 - t)}`;
+ }
+ path += 'Z';
+ return path;
+}
+
/** Flatten all `y` coordinates to `0` */
export function flattenPathData(pathData: string, yOverride = 0) {
let result = pathData;
diff --git a/packages/layerchart/src/lib/utils/shape.ts b/packages/layerchart/src/lib/utils/shape.ts
new file mode 100644
index 000000000..c22eb3611
--- /dev/null
+++ b/packages/layerchart/src/lib/utils/shape.ts
@@ -0,0 +1,91 @@
+import { range } from 'd3-array';
+import { degreesToRadians } from './math.js';
+
+/** Get points to create a polygon with given number of points and radius
+ *
+ * @param count - Number of points
+ * @param radius - Radius of the polygon
+ * @returns Array of points (angle, radius)
+ */
+export function polygonPoints(count: number, radius: number, rotate: number = 0) {
+ const angle = 360 / count;
+
+ return range(count).map((index) => {
+ return {
+ angle: degreesToRadians(angle * index) + degreesToRadians(rotate),
+ radius,
+ };
+ });
+}
+
+/** Create polygon
+ *
+ * @param cx - Center x coordinate
+ * @param cy - Center y coordinate
+ * @param count - Number of points
+ * @param radius - Radius of the polygon
+ * @param rotate - Rotation of the polygon (degrees)
+ * @param inset - Percent to inset odd points (<1 inset, >1 outset)
+ * @param scaleX - Horizontal stretch factor
+ * @param scaleY - Vertical stretch factor
+ * @param skewX - Skew angle in degrees along the X axis
+ * @param skewY - Skew angle in degrees along the Y axis
+ * @param tiltX - Tilt factor for x-coordinates (0 = no tilt, positive moves points top => down, negative moves points bottom => up)
+ * @param tiltY - Tilt factor for y-coordinates (0 = no tilt, positive moves points left => right, negative moves points right => left)
+ * @returns Array of points (x, y)
+ */
+export function polygon(options: {
+ cx: number;
+ cy: number;
+ count: number;
+ radius: number;
+ rotate?: number;
+ inset?: number;
+ scaleX?: number;
+ scaleY?: number;
+ skewX?: number;
+ skewY?: number;
+ tiltX?: number;
+ tiltY?: number;
+}) {
+ const {
+ cx,
+ cy,
+ count,
+ radius,
+ rotate = 0,
+ inset = 1,
+ scaleX = 1,
+ scaleY = 1,
+ skewX = 0,
+ skewY = 0,
+ tiltX = 0,
+ tiltY = 0,
+ } = options;
+ const skewXRad = degreesToRadians(skewX);
+ const skewYRad = degreesToRadians(skewY);
+ return polygonPoints(count, radius, rotate).map(({ angle, radius }, i) => {
+ // inset
+ const insetScale = i % 2 == 0 ? 1 : 1 - inset;
+
+ // scale
+ let x = radius * insetScale * Math.cos(angle) * scaleX;
+ let y = radius * insetScale * Math.sin(angle) * scaleY;
+
+ // tilt
+ const normalizedY = (y + radius) / (2 * radius);
+ const normalizedX = (x + radius) / (2 * radius);
+ const tiltScaleX = tiltX > 0 ? 1 + tiltX * (1 - normalizedY) : 1 - tiltX * normalizedY;
+ const tiltScaleY = tiltY > 0 ? 1 + tiltY * (1 - normalizedX) : 1 - tiltY * normalizedX;
+ x *= tiltScaleX;
+ y *= tiltScaleY;
+
+ // skew
+ const xSkewed = x + Math.tan(skewXRad) * y;
+ const ySkewed = y + Math.tan(skewYRad) * x;
+ return {
+ x: cx + xSkewed,
+ y: cy + ySkewed,
+ };
+ });
+}
diff --git a/packages/layerchart/src/routes/_NavMenu.svelte b/packages/layerchart/src/routes/_NavMenu.svelte
index 23450850e..f754b48f3 100644
--- a/packages/layerchart/src/routes/_NavMenu.svelte
+++ b/packages/layerchart/src/routes/_NavMenu.svelte
@@ -69,10 +69,12 @@
'Bar',
'Circle',
'Connector',
+ 'Ellipse',
'Group',
'Line',
'Marker',
'Point',
+ 'Polygon',
'Rect',
'Text',
],
diff --git a/packages/layerchart/src/routes/docs/components/BarChart/+page.svelte b/packages/layerchart/src/routes/docs/components/BarChart/+page.svelte
index 1f5ab1717..b40e3ba6d 100644
--- a/packages/layerchart/src/routes/docs/components/BarChart/+page.svelte
+++ b/packages/layerchart/src/routes/docs/components/BarChart/+page.svelte
@@ -13,6 +13,7 @@
Tooltip,
Circle,
Group,
+ Polygon,
} from 'layerchart';
import { extent, group, mean, sum } from 'd3-array';
import { scaleLinear, scaleLog, scaleThreshold, scaleTime } from 'd3-scale';
@@ -1188,7 +1189,7 @@
Single stack with indicator
-
+
- {@const width = 12}
- {@const height = 12}
-
-
+
{/snippet}
{#snippet tooltip({ context })}
diff --git a/packages/layerchart/src/routes/docs/components/Ellipse/+page.svelte b/packages/layerchart/src/routes/docs/components/Ellipse/+page.svelte
new file mode 100644
index 000000000..633b162d4
--- /dev/null
+++ b/packages/layerchart/src/routes/docs/components/Ellipse/+page.svelte
@@ -0,0 +1,26 @@
+
+
+Examples
+
+
+
+
diff --git a/packages/layerchart/src/routes/docs/components/Ellipse/+page.ts b/packages/layerchart/src/routes/docs/components/Ellipse/+page.ts
new file mode 100644
index 000000000..544a32480
--- /dev/null
+++ b/packages/layerchart/src/routes/docs/components/Ellipse/+page.ts
@@ -0,0 +1,14 @@
+import api from '$lib/components/Ellipse.svelte?raw&sveld';
+import source from '$lib/components/Ellipse.svelte?raw';
+import pageSource from './+page.svelte?raw';
+
+export async function load() {
+ return {
+ meta: {
+ api,
+ source,
+ pageSource,
+ description: '`` element with tweened properties using `motionStore`',
+ },
+ };
+}
diff --git a/packages/layerchart/src/routes/docs/components/Polygon/+page.svelte b/packages/layerchart/src/routes/docs/components/Polygon/+page.svelte
new file mode 100644
index 000000000..c750ba83d
--- /dev/null
+++ b/packages/layerchart/src/routes/docs/components/Polygon/+page.svelte
@@ -0,0 +1,327 @@
+
+
+Playground
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#snippet children({ context })}
+
+
+
+ {/snippet}
+
+
+
+Examples
+
+
+
Simple
+
+
+
+
+
+
+
Triangle
+
+
+
+ {#snippet children({ context })}
+
+
+
+ {/snippet}
+
+
+
+
+
+
+
Square
+
+
+
+ {#snippet children({ context })}
+
+
+
+ {/snippet}
+
+
+
+
+
+
+
Diamond
+
+
+
+ {#snippet children({ context })}
+
+
+
+ {/snippet}
+
+
+
+
+
+
+
Rhombus
+
+
+
+ {#snippet children({ context })}
+
+
+
+ {/snippet}
+
+
+
+
+
+
+
Parallelogram
+
+
+
+ {#snippet children({ context })}
+
+
+
+ {/snippet}
+
+
+
+
+
+
+
Trapezoid
+
+
+
+ {#snippet children({ context })}
+
+
+
+ {/snippet}
+
+
+
+
+
+
+
Pentagon
+
+
+
+ {#snippet children({ context })}
+
+
+
+ {/snippet}
+
+
+
+
+
+
+
Hexagon
+
+
+
+ {#snippet children({ context })}
+
+
+
+ {/snippet}
+
+
+
+
+
+
+
Octagon
+
+
+
+ {#snippet children({ context })}
+
+
+
+ {/snippet}
+
+
+
+
+
+
+
Octagon
+
+
+
+ {#snippet children({ context })}
+
+
+
+ {/snippet}
+
+
+
+
+
+
+
+
Stars
+
+
+
+
+
+
+ {#each [6, 8, 10, 12, 14, 16, 18, 20] as points}
+
+ {/each}
+
diff --git a/packages/layerchart/src/routes/docs/components/Polygon/+page.ts b/packages/layerchart/src/routes/docs/components/Polygon/+page.ts
new file mode 100644
index 000000000..7efff572c
--- /dev/null
+++ b/packages/layerchart/src/routes/docs/components/Polygon/+page.ts
@@ -0,0 +1,13 @@
+import api from '$lib/components/Polygon.svelte?raw&sveld';
+import source from '$lib/components/Polygon.svelte?raw';
+import pageSource from './+page.svelte?raw';
+
+export async function load() {
+ return {
+ meta: {
+ api,
+ source,
+ pageSource,
+ },
+ };
+}
diff --git a/packages/layerchart/src/routes/docs/components/Text/+page.svelte b/packages/layerchart/src/routes/docs/components/Text/+page.svelte
index 89666e161..3300d5e69 100644
--- a/packages/layerchart/src/routes/docs/components/Text/+page.svelte
+++ b/packages/layerchart/src/routes/docs/components/Text/+page.svelte
@@ -29,9 +29,7 @@
});
-Examples
-
-Playground
+Playground