Skip to content

Commit

Permalink
improve a11y of SVG elements; support server-rendered hydration; allo…
Browse files Browse the repository at this point in the history
…w for attributes to override set CSS variables
  • Loading branch information
chrisburnell committed Feb 6, 2024
1 parent 4706e1e commit 11edb18
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 26 deletions.
70 changes: 70 additions & 0 deletions demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,75 @@ <h2>Defined width and height</h2>
<p>
<svg-sparkline values="8,3,2,7,9,1,5,6,4,10,3,8,2,7,1,9" width="300" height="100"></svg-sparkline>
</p>
<h2>Server-rendered and hydrated</h2>
<p>
<svg-sparkline values="8,3,2,7,9,1,5,6,4,10,3,8,2,7,1,9" start-label="Start" end-label="End" animate="true">
<span>Start</span>
<svg width="200px" height="36px" viewBox="0 0 15 12" preserveAspectRatio="none" role="img">
<title>Sparkline ranging from 1 to 10.</title>
<path d="M 0,3 L 1,8 L 2,9 L 3,4 L 4,2 L 5,10 L 6,6 L 7,5 L 8,7 L 9,1 L 10,8 L 11,3 L 12,9 L 13,4 L 14,10 L 15,2" stroke="var(--svg-sparkline-color, currentColor)" stroke-width="2" stroke-linecap="round" fill="transparent" vector-effect="non-scaling-stroke"></path>
</svg>
<svg width="200px" height="36px" viewBox="0 0 200 36" preserveAspectRatio="xMaxYMid meet" aria-hidden="true">
<title>Sparkline Endpoint</title>
<circle r="3" cx="200" cy="6" fill="var(--svg-sparkline-endpoint-color, currentColor)"></circle>
</svg>
<span>End</span>
</svg-sparkline>
</p>
<style>
svg-sparkline:not(:defined) {
display: grid;
display: inline-grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr auto;
svg {
inline-size: auto;
grid-column: 1 / 3;
grid-row: 1 / 2;
padding: var(--svg-sparkline-padding, 0.375rem);
overflow: visible;
}
svg[aria-hidden] {
pointer-events: none;
}
span {
padding-inline: var(--svg-sparkline-padding, 0.375rem);
}
span:nth-of-type(1) {
grid-column: 1 / 2;
text-align: start;
}
span:nth-of-type(2) {
grid-column: 2 / 3;
text-align: end;
}
}
@media (prefers-reduced-motion: no-preference) {
svg-sparkline[animate]:not(:defined) {
--duration: var(--svg-sparkline-animation-duration, var(--animation-duration, 1s));
--first-delay: var(--svg-sparkline-animation-first-delay, var(--svg-sparkline-animation-delay, var(--animation-delay, 1s)));
--second-delay: var(--svg-sparkline-animation-second-delay, calc(var(--duration) + var(--first-delay)));
svg:first-of-type {
clip-path: polygon(0 0, 0 0, 0 100%, 0 100%);
animation: swipe var(--duration) linear var(--first-delay) forwards;
}
svg:last-of-type,
span {
opacity: 0;
animation: fadein var(--duration) linear var(--second-delay) forwards;
}
}
}
@keyframes swipe {
to {
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}
}
@keyframes fadein {
to {
opacity: 1;
}
}
</style>
</body>
</html>
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@chrisburnell/svg-sparkline",
"version": "1.0.4",
"version": "1.0.5",
"description": "A Web Component that builds an SVG Sparkline.",
"main": "svg-sparkline.js",
"scripts": {
Expand Down
66 changes: 41 additions & 25 deletions svg-sparkline.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ class SVGSparkline extends HTMLElement {
padding: var(--svg-sparkline-padding, 0.375rem);
overflow: visible;
}
svg[aria-hidden] {
pointer-events: none;
}
span {
padding-inline: var(--svg-sparkline-padding, 0.375rem);
}
Expand Down Expand Up @@ -82,40 +85,45 @@ class SVGSparkline extends HTMLElement {
this.values = this.getAttribute("values").split(",")
this.width = parseFloat(this.getAttribute("width")) || 200
this.height = parseFloat(this.getAttribute("height")) || 36
this.color = this.getAttribute("color") || "currentColor"
this.curve = this.getAttribute("curve") === "true"
this.color = this.getAttribute("color")
this.curve = this.hasAttribute("curve") && this.getAttribute("curve") !== "false"
this.endpoint = this.getAttribute("endpoint") !== "false"
this.endpointColor = this.getAttribute("endpoint-color") || this.color
this.endpointColor = this.getAttribute("endpoint-color")
this.endpointWidth = parseFloat(this.getAttribute("endpoint-width")) || 6
this.fill = this.getAttribute("fill") === "true"
this.gradient = this.getAttribute("gradient") === "true"
this.gradientColor = this.getAttribute("gradient-color") || this.getAttribute("fill-color") || this.color
this.fill = this.hasAttribute("fill") && this.getAttribute("fill") !== "false"
this.gradient = this.hasAttribute("gradient") && this.getAttribute("gradient") !== "false"
this.gradientColor = this.getAttribute("fill-color") || this.getAttribute("gradient-color")
this.lineWidth = parseFloat(this.getAttribute("line-width")) || 2
this.startLabel = this.getAttribute("start-label")
this.endLabel = this.getAttribute("end-label")

const color = this.color || `var(--svg-sparkline-color, currentColor)`
const endpointColor = this.endpointColor || `var(--svg-sparkline-endpoint-color, ${color})`
const gradientColor = this.gradientColor || `var(--svg-sparkline-fill-color, var(--svg-sparkline-gradient-color, ${color}))`

let content = []

if (this.startLabel) {
content.push(`<span>${this.startLabel}</span>`)
}

content.push(`
<svg width="${this.width}px" height="${this.height}px" viewBox="${this.getViewBox(this.values)}" preserveAspectRatio="none">
<svg width="${this.width}px" height="${this.height}px" viewBox="${this.getViewBox(this.values)}" preserveAspectRatio="none" role="img">
<title>Sparkline ranging from ${this.getMinY(this.values)} to ${this.getMaxY(this.values)}.</title>
`)

if (this.gradient || this.fill) {
const gradientID = this.makeID(6)
content.push(`
<defs>
<linearGradient id="svg-sparkline-gradient-${gradientID}" gradientTransform="rotate(90)">
<stop offset="0%" stop-color="var(--svg-sparkline-gradient-color, ${this.gradientColor})" />
<stop offset="0%" stop-color="${gradientColor}" />
<stop offset="100%" stop-color="transparent" />
</linearGradient>
</defs>
<path
d="${this.getPath(this.values, this.curve ? this.bezierCommand : this.lineCommand)} L ${this.getFinalX(this.values)} ${this.getHighestY(this.values)} L 0 ${this.getHighestY(this.values)} Z"
fill="${this.fill ? `var(--svg-sparkline-gradient-color, ${this.gradientColor})` : `url('#svg-sparkline-gradient-${gradientID}')`}"
d="${this.getPath(this.values, this.curve ? this.bezierCommand : this.lineCommand)} L ${this.getFinalX(this.values)} ${this.getAdjustedMaxY(this.values)} L 0 ${this.getAdjustedMaxY(this.values)} Z"
fill="${this.fill ? gradientColor : `url('#svg-sparkline-gradient-${gradientID}')`}"
stroke="transparent"
/>
`)
Expand All @@ -124,7 +132,7 @@ class SVGSparkline extends HTMLElement {
content.push(`
<path
d="${this.getPath(this.values, this.curve ? this.bezierCommand : this.lineCommand)}"
stroke="var(--svg-sparkline-color, ${this.color})"
stroke="${color}"
stroke-width="${this.lineWidth}"
stroke-linecap="round"
fill="transparent"
Expand All @@ -136,8 +144,8 @@ class SVGSparkline extends HTMLElement {

if (this.endpoint) {
content.push(`
<svg width="${this.width}px" height="${this.height}px" viewBox="0 0 ${this.width} ${this.height}" preserveAspectRatio="xMaxYMid meet">
<circle " r="${this.endpointWidth / 2}" cx="${this.width}" cy="${(this.height / this.getHighestY(this.values)) * this.getFinalY(this.values)}" fill="var(--svg-sparkline-endpoint-color, ${this.endpointColor})"></circle>
<svg width="${this.width}px" height="${this.height}px" viewBox="0 0 ${this.width} ${this.height}" preserveAspectRatio="xMaxYMid meet" aria-hidden="true">
<circle r="${this.endpointWidth / 2}" cx="${this.width}" cy="${(this.height / this.getAdjustedMaxY(this.values)) * this.getFinalY(this.values)}" fill="${endpointColor}"></circle>
</svg>
`)
}
Expand Down Expand Up @@ -181,7 +189,12 @@ class SVGSparkline extends HTMLElement {

initTemplate() {
if (this.shadowRoot) {
this.shadowRoot.innerHTML = this.render()
if (this.innerHTML.trim() === "") {
this.shadowRoot.innerHTML = this.render()
} else {
this.shadowRoot.innerHTML = this.innerHTML
this.innerHTML = ""
}
return
}

Expand Down Expand Up @@ -283,20 +296,23 @@ class SVGSparkline extends HTMLElement {
return Math.max(...values) - values[values.length - 1] + 1
}

getHighestY(values) {
return Math.max(...values) + 2
getMinY(values) {
return Math.min(...values)
}

getMaxY(values) {
return Math.max(...values)
}

getAdjustedMaxY(values) {
return this.getMaxY(values) + 2
}

makeID(length) {
let result = ""
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
const charactersLength = characters.length
let counter = 0
while (counter < length) {
result += characters.charAt(Math.floor(Math.random() * charactersLength))
counter += 1
}
return result
const SEQUENCE = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
return Array.from({ length: length }).reduce((id, _) => {
return id + SEQUENCE.charAt(Math.floor(Math.random() * SEQUENCE.length))
}, "")
}
}

Expand Down

0 comments on commit 11edb18

Please sign in to comment.