Skip to content

Commit

Permalink
add class property, angles in degree, fix styles
Browse files Browse the repository at this point in the history
  • Loading branch information
vnau committed Dec 7, 2024
1 parent 2f3da88 commit 767dc06
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 115 deletions.
33 changes: 10 additions & 23 deletions src/lib/Gauge.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
}

.gauge-circle {
fill: white;
fill: none;
stroke: var(--stroke-color, lightgray);
opacity: 0.3;
stroke-width: var(--gauge-stroke);
Expand Down Expand Up @@ -39,35 +39,22 @@
transition: r 0.3s ease-out;
}

.gauge-title-curve {
fill: none;
stroke: none;
}

.titles-container {
color: currentColor;
font-family: "Calibri", sans-serif;
font-size: 20px;
font-weight: 200;

text:not(:first-of-type) {
color: lightslategray;
font-size: 14px;
}
fill: currentcolor;
}

.slot-container {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
position: relative;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: calc(var(--gauge-stroke) + var(--gauge-border));
line-height: normal;
text-align: center;
padding: calc(var(--gauge-stroke) + var(--gauge-border))
}

.slot-content {
font-size: calc((var(--gauge-radius) - 2 * (var(--gauge-stroke) - var(--gauge-border)))/2);
font-family: 'Calibri';
font-weight: 100;
fill: currentColor;
font-size: calc((var(--gauge-radius) - 2 * (var(--gauge-stroke) - var(--gauge-border)))/3);
}
139 changes: 75 additions & 64 deletions src/lib/Gauge.svelte
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
<script lang="ts">
import { onMount } from "svelte";
import { tweened } from "svelte/motion";
import { elasticOut } from "svelte/easing";
import { cubicOut } from "svelte/easing";
import {
calcCurvePath,
getTitleAngle,
getTitleOffset,
polarToCartesian,
valueToAngle,
scale,
} from "./util.js";
export let easing: (v: number) => number = elasticOut;
export let easing: (v: number) => number = cubicOut;
export let value: number | undefined;
export let width: number | string | undefined = undefined;
export let start: number = 0;
export let stop: number = 100;
export let startAngle: number = 0;
export let stopAngle: number = 2 * Math.PI;
export let stopAngle: number = 360;
export let titleAngle: number = 0;
let className: string | undefined = undefined;
export let stroke: number = 20;
export let titles: string[] = [];
export let ranges: [number, number][] = [];
let className: string | undefined = undefined;
export { className as class };
let clientWidth: number;
let clientHeight: number;
Expand Down Expand Up @@ -65,10 +65,66 @@
{@const handlePos = polarToCartesian(
radius,
borderAdjusted,
valueToAngle($animatedValue, start, stop, startAngle, stopAngle)
(scale($animatedValue, start, stop, startAngle, stopAngle) * Math.PI) /
180
)}

<svg class="gauge-svg" xmlns="http://www.w3.org/2000/svg">
<defs>
{#each ranges as range, index}
<path
id={`range-${index}-${uuid}`}
d={calcCurvePath(
radius,
borderAdjusted,
scale(range[0], start, stop, startAngle, stopAngle),
scale(range[1], start, stop, startAngle, stopAngle)
)}
/>
{/each}
<path
id="title-path-{uuid}"
d={calcCurvePath(
radius,
border - 2,
titleAngle + startAngle - (stopAngle - startAngle) / 2.001,
titleAngle + stopAngle + (stopAngle - startAngle) / 2.001
)}
/>
</defs>

<!-- Titles -->
{#if visible}
<text in:spin class="titles-container">
{#each titles as title, index}
<textPath
xlink:href="#title-path-{uuid}"
startOffset="{getTitleOffset(
startAngle,
stopAngle,
index,
titles.length
)}%"
text-anchor="middle"
>
{title}
</textPath>
{/each}
</text>
{#if !$$slots.default}
<text
class="slot-content"
x="50%"
y="50%"
dominant-baseline="middle"
text-anchor="middle"
>{$animatedValue === 0 && value === undefined
? "NaN"
: Math.round($animatedValue)}</text
>
{/if}
{/if}

<!-- Background Circle -->
<path
class="gauge-circle"
Expand All @@ -83,23 +139,13 @@
radius,
borderAdjusted,
startAngle,
valueToAngle($animatedValue, start, stop, startAngle, stopAngle)
scale($animatedValue, start, stop, startAngle, stopAngle)
)}
/>
{/if}
{#each ranges as range, index}
{#each ["gauge-range-bg", "gauge-range"] as cls, index}
<path
id={`${cls}${index}`}
class={cls}
d={calcCurvePath(
radius,
borderAdjusted,
valueToAngle(range[0], start, stop, startAngle, stopAngle),
valueToAngle(range[1], start, stop, startAngle, stopAngle)
)}
/>
{/each}
<use href="#range-{index}-{uuid}" class="gauge-range-bg"></use>
<use href="#range-{index}-{uuid}" class="gauge-range"></use>
{/each}

<!-- Handle -->
Expand All @@ -111,51 +157,16 @@
r={stroke / 2}
/>
{/if}

<!-- Titles -->
{#if visible}
<g in:spin class="titles-container">
{#each titles as title, index}
<path
class="gauge-title-curve"
id={`title-${uuid}-${index}`}
d={calcCurvePath(
radius,
border - 2,
titleAngle +
getTitleAngle(startAngle, stopAngle, index, titles.length) -
Math.PI / 2,
titleAngle +
getTitleAngle(startAngle, stopAngle, index, titles.length) +
Math.PI / 2
)}
/>
<text>
<textPath
xlink:href={`#title-${uuid}-${index}`}
startOffset="50%"
text-anchor="middle"
>
{title}
</textPath>
</text>
{/each}
</g>
{/if}
</svg>
<div class="slot-container">
<slot
value={$animatedValue === 0 && value === undefined
? undefined
: $animatedValue}
>
<span class="slot-content"
>{$animatedValue === 0 && value === undefined
? "NaN"
: Math.round($animatedValue)}</span
>
</slot>
</div>
{#if !!$$slots.default}
<div class="slot-container">
<slot
value={$animatedValue === 0 && value === undefined
? undefined
: $animatedValue}
/>
</div>
{/if}
{/if}
</div>

Expand Down
73 changes: 45 additions & 28 deletions src/lib/util.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,55 @@
// Clamp value between min and max
export const clamp = (val: number, minVal: number, maxVal: number) =>
Math.max(minVal, Math.min(maxVal, val));
export const clamp = (value: number, min: number, max: number): number =>
Math.max(min, Math.min(max, value));

// Map value to an angle (in radians)
export const valueToAngle = (value: number, min: number, max: number, startAngle: number, stopAngle: number) =>
startAngle + (stopAngle - startAngle) * (clamp(value, min, max) - min) / (max - min);
// Scale value
export const scale = (value: number, start: number, stop: number, targetStart: number, targetStop: number): number =>
targetStart + (targetStop - targetStart) * (clamp(value, start, stop) - start) / (stop - start);

// Calculate title offset based on index and total count
export const getTitleOffset = (startAngle: number, stopAngle: number, index: number, count: number): number => {
const fullCircle = 360;
const isCircleGauge = Math.abs(stopAngle - startAngle - fullCircle) < 0.001;
const normalizedIndex = index / (count - (isCircleGauge || count === 1 ? 0 : 1));
const angle = normalizedIndex * fullCircle;

// Offset calculation based on proportional angle
return Math.abs(25 + (50 * angle) / fullCircle);
};

export const getTitleAngle = (startAngle: number, stopAngle: number, index: number, count: number) => {
const isCircleGauge = Math.abs(Math.abs(stopAngle - startAngle) - 2 * Math.PI) < 0.001;
return startAngle + (index / (count - (isCircleGauge ? 0 : 1))) * (stopAngle - startAngle)
}
// Convert polar coordinates to Cartesian
export const polarToCartesian = (radius: number, offset: number, angle: number) => {
export const polarToCartesian = (radius: number, offset: number, angle: number): { x: string; y: string } => {
const adjustedRadius = radius - offset;
return {
x: (radius - adjustedRadius * Math.sin(angle)).toFixed(2),
y: (radius + adjustedRadius * Math.cos(angle)).toFixed(2),
x: (radius - adjustedRadius * Math.sin(angle)).toFixed(3),
y: (radius + adjustedRadius * Math.cos(angle)).toFixed(3),
};
};

// Generate SVG path for an arc
export const calcCurvePath = (
radius: number,
offset: number,
startAngle: number,
stopAngle: number
) => {
if (stopAngle - startAngle >= 2 * Math.PI)
stopAngle = startAngle + 2 * Math.PI - 0.0001;
const start = polarToCartesian(radius, offset, startAngle);
const end = polarToCartesian(radius, offset, stopAngle);
const largeArc = stopAngle - startAngle > Math.PI ? 1 : 0;
const sweepFlag = stopAngle > startAngle ? 1 : 0;
return `M ${start.x} ${start.y} A
${radius - offset} ${radius - offset}
0 ${largeArc} ${sweepFlag} ${end.x} ${end.y}`;
};
export const calcCurvePath = (radius: number, offset: number, startAngle: number, endAngle: number): string => {
const startRad = startAngle * Math.PI / 180;
const endRad = endAngle * Math.PI / 180;

const startPoint = polarToCartesian(radius, offset, startRad);

// Helper to generate an arc command for SVG path
const arcCommand = (fromAngle: number, toAngle: number): string => {
toAngle = Math.min(toAngle, fromAngle + Math.PI * 1.9999)
const endPoint = polarToCartesian(radius, offset, toAngle);
const largeArcFlag = toAngle - fromAngle > Math.PI ? 1 : 0;
const sweepFlag = toAngle > fromAngle ? 1 : 0;
return `A ${radius - offset} ${radius - offset} 0 ${largeArcFlag} ${sweepFlag} ${endPoint.x} ${endPoint.y}`;
};

// Determine whether to split into two arcs for full circles
let cmd = `M ${startPoint.x} ${startPoint.y}`;
let arcStart = startRad
do {
const arcStop = arcStart + Math.min(Math.PI * 2, endRad - arcStart)
cmd += ` ${arcCommand(arcStart, arcStop)}`;
arcStart = arcStop;
} while (arcStart < endRad)

return cmd;
};

0 comments on commit 767dc06

Please sign in to comment.