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} +
+

{points} point

+ +
+ + + + + +
+
+
+ {/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