tiny-spark 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENCE +21 -0
- package/README.md +40 -0
- package/dist/tiny-spark.es.js +234 -0
- package/dist/tiny-spark.umd.js +8 -0
- package/dist/vite.svg +1 -0
- package/index.html +32 -0
- package/package.json +29 -0
- package/public/vite.svg +1 -0
- package/src/index.ts +49 -0
- package/src/lib.ts +334 -0
- package/src/main.ts +33 -0
- package/src/style.css +111 -0
- package/src/svg.ts +126 -0
- package/src/typescript.svg +1 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +24 -0
- package/types/index.ts +42 -0
- package/vite.config.ts +18 -0
package/LICENCE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ALEC LLOYD PROBERT
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# tiny-spark
|
|
2
|
+
|
|
3
|
+
[](https://github.com/graphieros/tiny-spark)
|
|
4
|
+
[](https://github.com/graphieros/tiny-spark/issues)
|
|
5
|
+
[](https://github.com/graphieros/tiny-spark?tab=MIT-1-ov-file#readme)
|
|
6
|
+
[](https://github.com/graphieros/tiny-spark)
|
|
7
|
+
|
|
8
|
+
An elegant, reactive and responsive sparkline chart solution without dependency.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
npm i tiny-spark
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
Just set up a div with a "tiny-spark" class, with a few data attributes to configure the chart.
|
|
19
|
+
|
|
20
|
+
```html
|
|
21
|
+
<div style="width: 100%">
|
|
22
|
+
<div
|
|
23
|
+
class="tiny-spark"
|
|
24
|
+
data-curve="true"
|
|
25
|
+
data-set="[1, 2, 3, 5, 8, 13]"
|
|
26
|
+
data-dates='["01-2026", "02-2026", "03-2026", "04-2026", "05-2026", "06-2026"]'
|
|
27
|
+
data-responsive
|
|
28
|
+
data-animation="true"
|
|
29
|
+
data-line-color="#4A4A4A"
|
|
30
|
+
data-area-color="#1A1A1A10"
|
|
31
|
+
data-line-thickness="4"
|
|
32
|
+
data-plot-color="#2A2A2A"
|
|
33
|
+
data-plot-radius="2"
|
|
34
|
+
data-number-locale="en-US"
|
|
35
|
+
data-number-rounding="2"
|
|
36
|
+
data-indicator-color="#1A1A1A"
|
|
37
|
+
data-indicator-width="1"
|
|
38
|
+
></div>
|
|
39
|
+
</div>
|
|
40
|
+
```
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
const O = "http://www.w3.org/2000/svg";
|
|
2
|
+
var F = /* @__PURE__ */ ((t) => (t.BAR = "data-bar", t.LINE = "data-line", t.SET = "data-set", t.RESPONSIVE = "data-responsive", t))(F || {}), h = /* @__PURE__ */ ((t) => (t.ANIMATION = "animation", t.AREA_COLOR = "areaColor", t.CURVE = "curve", t.DATES = "dates", t.INDICATOR_COLOR = "indicatorColor", t.INDICATOR_WIDTH = "indicatorWidth", t.LINE_COLOR = "lineColor", t.LINE_THICKNESS = "lineThickness", t.NUMBER_LOCALE = "numberLocale", t.NUMBER_ROUNDING = "numberRounding", t.NUMBER_SHOW_ON = "numberShowOn", t.PLOT_COLOR = "plotColor", t.PLOT_RADIUS = "plotRadius", t.SET = "set", t))(h || {});
|
|
3
|
+
function G(t) {
|
|
4
|
+
const { width: r, height: e } = t.parentElement.getBoundingClientRect(), o = { width: 300, height: 100 }, a = `0 0 ${r || o.width} ${e || o.height}`, i = document.createElementNS(O, "svg"), u = X();
|
|
5
|
+
return i.id = u, i.setAttribute("viewBox", a), i.style.width = "100%", i.style.height = "100%", {
|
|
6
|
+
svg: i,
|
|
7
|
+
svgId: u,
|
|
8
|
+
width: r || o.width,
|
|
9
|
+
height: e || o.height
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function x(t, r = 0) {
|
|
13
|
+
return isNaN(t) ? r : t;
|
|
14
|
+
}
|
|
15
|
+
function M(t) {
|
|
16
|
+
let r = [];
|
|
17
|
+
for (let e = 0; e < t.length; e += 1)
|
|
18
|
+
r.push(`${x(t[e].x)},${x(t[e].y)} `);
|
|
19
|
+
return r.join(" ").trim();
|
|
20
|
+
}
|
|
21
|
+
function R(t) {
|
|
22
|
+
if (t.length < 1) return "0,0";
|
|
23
|
+
const r = t.length - 1, e = [`${x(t[0].x)},${x(t[0].y)}`], o = [], a = [], i = [], u = [];
|
|
24
|
+
for (let n = 0; n < r; n += 1)
|
|
25
|
+
o[n] = t[n + 1].x - t[n].x, a[n] = t[n + 1].y - t[n].y, i[n] = a[n] / o[n];
|
|
26
|
+
u[0] = i[0], u[r] = i[r - 1];
|
|
27
|
+
for (let n = 1; n < r; n += 1)
|
|
28
|
+
if (i[n - 1] * i[n] <= 0)
|
|
29
|
+
u[n] = 0;
|
|
30
|
+
else {
|
|
31
|
+
const d = 2 * i[n - 1] * i[n] / (i[n - 1] + i[n]);
|
|
32
|
+
u[n] = d;
|
|
33
|
+
}
|
|
34
|
+
for (let n = 0; n < r; n += 1) {
|
|
35
|
+
const d = t[n].x, g = t[n].y, c = t[n + 1].x, C = t[n + 1].y, A = u[n], v = u[n + 1], $ = d + (c - d) / 3, S = g + A * (c - d) / 3, I = c - (c - d) / 3, y = C - v * (c - d) / 3;
|
|
36
|
+
e.push(`C ${x($)},${x(S)} ${x(I)},${x(y)} ${x(c)},${x(C)}`);
|
|
37
|
+
}
|
|
38
|
+
return e.join(" ");
|
|
39
|
+
}
|
|
40
|
+
function U(t, r = 1e3) {
|
|
41
|
+
const e = t.getTotalLength();
|
|
42
|
+
t.style.strokeDasharray = String(e), t.style.strokeDashoffset = String(e), t.getBoundingClientRect(), t.style.transition = `stroke-dashoffset ${r}ms ease-in-out`, t.style.strokeDashoffset = "0", t.addEventListener("transitionend", function o() {
|
|
43
|
+
t.style.transition = "", t.removeEventListener("transitionend", o);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function W(t, r, e = 1e3) {
|
|
47
|
+
const o = r.getBBox(), a = o.width, i = document.createElementNS("http://www.w3.org/2000/svg", "clipPath"), u = "clip-" + Math.random().toString(36).substr(2, 9);
|
|
48
|
+
i.setAttribute("id", u);
|
|
49
|
+
const n = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
50
|
+
n.setAttribute("x", o.x.toString()), n.setAttribute("y", o.y.toString()), n.setAttribute("width", "0"), n.setAttribute("height", o.height.toString()), i.appendChild(n);
|
|
51
|
+
let d = t.querySelector("defs");
|
|
52
|
+
d || (d = document.createElementNS("http://www.w3.org/2000/svg", "defs"), t.insertBefore(d, t.firstChild)), d.appendChild(i), r.setAttribute("clip-path", `url(#${u})`), n.style.transition = `width ${e}ms ease-out`, n.getBoundingClientRect(), n.setAttribute("width", a.toString()), n.addEventListener("transitionend", function g() {
|
|
53
|
+
r.removeAttribute("clip-path"), i.parentNode && i.parentNode.removeChild(i), n.removeEventListener("transitionend", g);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function j() {
|
|
57
|
+
return document.querySelectorAll(".tiny-spark");
|
|
58
|
+
}
|
|
59
|
+
function V(t, r) {
|
|
60
|
+
return Object.keys(t.dataset).includes(r);
|
|
61
|
+
}
|
|
62
|
+
function b(t, r, e) {
|
|
63
|
+
return V(t, r) ? t.dataset[r] : e;
|
|
64
|
+
}
|
|
65
|
+
function q(t, r) {
|
|
66
|
+
const e = new MutationObserver((o) => {
|
|
67
|
+
for (const a of o)
|
|
68
|
+
if (a.type === "attributes" && a.attributeName && Object.values(F).includes(a.attributeName)) {
|
|
69
|
+
r();
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return e.observe(t, { attributes: !0 }), e;
|
|
74
|
+
}
|
|
75
|
+
function K(t) {
|
|
76
|
+
if (!t) return {
|
|
77
|
+
color: "#1A1A1A",
|
|
78
|
+
backgroundColor: "#FFFFFF"
|
|
79
|
+
};
|
|
80
|
+
const r = window.getComputedStyle(t), e = r.getPropertyValue("color") || "#1A1A1A", o = r.getPropertyValue("background-color"), a = r.getPropertyValue("background");
|
|
81
|
+
return { color: e, backgroundColor: o || a || "#FFFFFF" };
|
|
82
|
+
}
|
|
83
|
+
function X() {
|
|
84
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(t) {
|
|
85
|
+
const r = Math.random() * 16 | 0;
|
|
86
|
+
return (t == "x" ? r : r & 3 | 8).toString(16);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function Y(t) {
|
|
90
|
+
const r = t.getAttribute("data-set");
|
|
91
|
+
if (!r) return [];
|
|
92
|
+
try {
|
|
93
|
+
const e = JSON.parse(r);
|
|
94
|
+
return Array.isArray(e) && e.every((o) => typeof o == "number" || [null, void 0].includes(o)) ? e : (console.warn("data-set is not an array of numbers."), []);
|
|
95
|
+
} catch (e) {
|
|
96
|
+
return console.error("Error parsing data-set:", e), [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function J(t) {
|
|
100
|
+
const r = t.getAttribute("data-dates");
|
|
101
|
+
if (!r) return [];
|
|
102
|
+
try {
|
|
103
|
+
const e = JSON.parse(r);
|
|
104
|
+
return Array.isArray(e) && e.every((o) => typeof o == "string") ? e : (console.warn("data-dates is not an array of strings"), []);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
return console.error("Error parsing data-dates", e), [];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function P(t) {
|
|
110
|
+
return {
|
|
111
|
+
min: Math.min(...t),
|
|
112
|
+
max: Math.max(...t)
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function B() {
|
|
116
|
+
return new Promise((t) => setTimeout(t, 0));
|
|
117
|
+
}
|
|
118
|
+
function Z(t, r) {
|
|
119
|
+
const e = String(b(t, h.NUMBER_LOCALE, navigator.language || "en-US")), o = Number(String(b(t, h.NUMBER_ROUNDING, 0)));
|
|
120
|
+
return r.toLocaleString(e, {
|
|
121
|
+
useGrouping: !0,
|
|
122
|
+
minimumFractionDigits: o,
|
|
123
|
+
maximumFractionDigits: o
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function z(t, r, e) {
|
|
127
|
+
if (!t.createSVGPoint || !t.getScreenCTM)
|
|
128
|
+
throw new Error("Your browser does not support SVG coordinate transformation.");
|
|
129
|
+
const o = t.getScreenCTM();
|
|
130
|
+
if (!o)
|
|
131
|
+
throw new Error("Cannot obtain the screen CTM.");
|
|
132
|
+
const a = t.createSVGPoint();
|
|
133
|
+
a.x = r, a.y = e;
|
|
134
|
+
const i = a.matrixTransform(o);
|
|
135
|
+
return { x: i.x, y: i.y };
|
|
136
|
+
}
|
|
137
|
+
function D(t, r, e, o, a) {
|
|
138
|
+
if (Q(o), !a) return;
|
|
139
|
+
const { x: i, y: u } = z(t, e.x, e.y), n = document.createElement("div");
|
|
140
|
+
n.classList.add("tiny-spark-tooltip"), n.setAttribute("id", `tooltip_${o}`), n.style.pointerEvents = "none", n.style.position = "fixed", n.style.top = u + "px", n.style.left = i + "px", n.style.width = "fit-content", n.innerHTML = `
|
|
141
|
+
<div class="tiny-spark-tooltip-content">${e.d ? `${e.d}: ` : ""}${[null, void 0].includes(e.v) ? "-" : Z(r, Number(e.v))}</div>
|
|
142
|
+
`, document.body.appendChild(n), B().then(() => {
|
|
143
|
+
const { width: d, height: g } = n.getBoundingClientRect();
|
|
144
|
+
n.style.left = `${i - d / 2}px`, n.style.top = `${u - g - Number(String(Number(b(r, h.PLOT_RADIUS, 3)) * 1.5))}px`;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function Q(t) {
|
|
148
|
+
const r = document.getElementById(`tooltip_${t}`);
|
|
149
|
+
r == null || r.remove();
|
|
150
|
+
}
|
|
151
|
+
function T(t) {
|
|
152
|
+
t.innerHTML = "";
|
|
153
|
+
}
|
|
154
|
+
function tt(t, r) {
|
|
155
|
+
let e = r;
|
|
156
|
+
T(t);
|
|
157
|
+
const { svg: o, svgId: a, width: i, height: u } = G(t), { color: n, backgroundColor: d } = K(t), g = { T: 12, R: 12, B: 12, L: 12 }, c = {
|
|
158
|
+
left: g.L,
|
|
159
|
+
top: g.T,
|
|
160
|
+
width: i - g.L - g.R,
|
|
161
|
+
height: u - g.T - g.B,
|
|
162
|
+
bottom: u - g.B
|
|
163
|
+
}, C = Y(t), { min: A } = P(C), v = C.map((l) => [null, void 0].includes(l) ? l : l + (A < 0 ? Math.abs(A) : 0)), { max: $ } = P(v), S = c.width / (C.length - 1) === 1 / 0 ? c.width : c.width / (C.length - 1), I = J(t), y = v.map((l, m) => {
|
|
164
|
+
const s = {
|
|
165
|
+
w: v.length === 1 ? S / 2 : 0,
|
|
166
|
+
h: v.length === 1 ? c.height / 2 : 0
|
|
167
|
+
};
|
|
168
|
+
return {
|
|
169
|
+
y: (1 - (l || 0) / $) * c.height + g.T + s.h,
|
|
170
|
+
x: c.left + S * m + s.w,
|
|
171
|
+
v: l,
|
|
172
|
+
d: I[m] || null
|
|
173
|
+
};
|
|
174
|
+
}), w = [...y].filter(({ v: l }) => ![null, void 0].includes(l)), N = document.createElementNS(O, "path");
|
|
175
|
+
N.classList.add("tiny-spark-line-path"), !t.dataset.curve || t.dataset.curve === "true" ? N.setAttribute("d", `M ${R(w)}`) : N.setAttribute("d", `M ${M(w)}`), N.setAttribute("fill", "none"), N.setAttribute("stroke", String(b(t, h.LINE_COLOR, n))), N.setAttribute("stroke-width", String(b(t, h.LINE_THICKNESS, 2))), N.setAttribute("stroke-linecap", "round");
|
|
176
|
+
const L = document.createElementNS(O, "path");
|
|
177
|
+
L.classList.add("tiny-spark-line-area"), y.length && (!t.dataset.curve || t.dataset.curve === "true" ? L.setAttribute("d", `M ${w[0].x},${c.bottom} ${R(w)} L ${w.at(-1).x},${c.bottom} Z`) : L.setAttribute("d", `M ${w[0].x},${c.bottom} ${M(w)} L ${w.at(-1).x},${c.bottom} Z`)), L.setAttribute("fill", String(b(t, h.AREA_COLOR, "transparent"))), y.length > 1 && (o.appendChild(L), o.appendChild(N));
|
|
178
|
+
const H = t.getAttribute("data-animation"), k = [];
|
|
179
|
+
y.forEach((l, m) => {
|
|
180
|
+
const s = document.createElementNS(O, "line");
|
|
181
|
+
s.classList.add("tiny-spark-indicator"), s.setAttribute("id", `indicator_${a}_${m}`), s.setAttribute("x1", String(c.left + (y.length === 1 ? c.width / 2 : m * S))), s.setAttribute("x2", String(c.left + (y.length === 1 ? c.width / 2 : m * S))), s.setAttribute("y1", String(c.top)), s.setAttribute("y2", String(c.bottom)), s.setAttribute("stroke", String(b(t, h.INDICATOR_COLOR, "#1A1A1A"))), s.setAttribute("stroke-width", String(b(t, h.INDICATOR_WIDTH, "1"))), s.setAttribute("stroke-linecap", "round"), s.style.pointerEvents = "none", s.style.opacity = "0", k.push(s), o.appendChild(s);
|
|
182
|
+
});
|
|
183
|
+
let _ = [];
|
|
184
|
+
Number(String(b(t, h.PLOT_RADIUS, 0))) > 0 && y.forEach(({ x: l, y: m, v: s }, p) => {
|
|
185
|
+
if (![null, void 0].includes(s)) {
|
|
186
|
+
const f = document.createElementNS(O, "circle");
|
|
187
|
+
f.classList.add("tiny-spark-datapoint-circle"), f.classList.add(`circle-${a}`), f.setAttribute("id", `circle_${a}_${p}`), f.setAttribute("cx", String(l)), f.setAttribute("cy", String(m)), f.setAttribute("r", String(b(t, h.PLOT_RADIUS, 3))), f.setAttribute("fill", String(b(t, h.PLOT_COLOR, String(b(t, "lineColor", n))))), f.setAttribute("stroke", d), f.style.transition = `opacity ${p * (1e3 * 2 / y.length)}ms ease-in`, f.style.opacity = "0", o.appendChild(f), _.push(f);
|
|
188
|
+
}
|
|
189
|
+
}), y.forEach((l, m) => {
|
|
190
|
+
const s = document.createElementNS(O, "rect");
|
|
191
|
+
s.classList.add("tiny-spark-tooltip-trap"), s.setAttribute("x", `${y.length === 1 ? 0 : c.left + m * S - S / 2}`), s.setAttribute("y", `${c.top}`), s.setAttribute("height", `${c.height}`), s.setAttribute("width", `${S}`), s.setAttribute("fill", "transparent"), s.addEventListener("mouseenter", () => {
|
|
192
|
+
D(o, t, l, a, !0);
|
|
193
|
+
const p = document.getElementById(`circle_${a}_${m}`);
|
|
194
|
+
p == null || p.setAttribute("r", String(Number(b(t, h.PLOT_RADIUS, 3)) * 1.5)), k[m].style.opacity = "1";
|
|
195
|
+
}), s.addEventListener("mouseout", () => {
|
|
196
|
+
D(o, t, l, a, !1);
|
|
197
|
+
const p = document.getElementById(`circle_${a}_${m}`);
|
|
198
|
+
p == null || p.setAttribute("r", String(Number(b(t, h.PLOT_RADIUS, 3)))), k.forEach((f) => f.style.opacity = "0");
|
|
199
|
+
}), o.appendChild(s);
|
|
200
|
+
}), H === "true" && e ? B().then(() => {
|
|
201
|
+
_.forEach((l) => {
|
|
202
|
+
l.style.opacity = "1";
|
|
203
|
+
}), U(N), W(o, L);
|
|
204
|
+
}) : _.forEach((l) => {
|
|
205
|
+
l.style.opacity = "1";
|
|
206
|
+
}), t.appendChild(o);
|
|
207
|
+
}
|
|
208
|
+
(function() {
|
|
209
|
+
window.addEventListener("load", () => {
|
|
210
|
+
const r = j();
|
|
211
|
+
r.length && Array.from(r).forEach((e) => {
|
|
212
|
+
et(e), e.__renderCount = 0, E(e), q(e, () => E(e)), new ResizeObserver((i) => {
|
|
213
|
+
i.forEach((u) => {
|
|
214
|
+
E(e);
|
|
215
|
+
});
|
|
216
|
+
}).observe(e.parentElement), new MutationObserver((i) => {
|
|
217
|
+
i.forEach((u) => {
|
|
218
|
+
u.type === "attributes" && u.attributeName && u.attributeName.startsWith("data-") && E(e);
|
|
219
|
+
});
|
|
220
|
+
}).observe(e, { attributes: !0 });
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
})();
|
|
224
|
+
function E(t) {
|
|
225
|
+
t.__renderCount += 1, V(t, "set") && tt(t, t.__renderCount < 3);
|
|
226
|
+
}
|
|
227
|
+
function et(t) {
|
|
228
|
+
t.dataset.set || console.error(`Tiny-spark exception:
|
|
229
|
+
|
|
230
|
+
[data-set] data attribute is missing.
|
|
231
|
+
Provide an array of numbers, for example:
|
|
232
|
+
|
|
233
|
+
data-set="[1, 2, 3]"`);
|
|
234
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
(function(S){typeof define=="function"&&define.amd?define(S):S()})(function(){"use strict";const S="http://www.w3.org/2000/svg";var M=(t=>(t.BAR="data-bar",t.LINE="data-line",t.SET="data-set",t.RESPONSIVE="data-responsive",t))(M||{}),g=(t=>(t.ANIMATION="animation",t.AREA_COLOR="areaColor",t.CURVE="curve",t.DATES="dates",t.INDICATOR_COLOR="indicatorColor",t.INDICATOR_WIDTH="indicatorWidth",t.LINE_COLOR="lineColor",t.LINE_THICKNESS="lineThickness",t.NUMBER_LOCALE="numberLocale",t.NUMBER_ROUNDING="numberRounding",t.NUMBER_SHOW_ON="numberShowOn",t.PLOT_COLOR="plotColor",t.PLOT_RADIUS="plotRadius",t.SET="set",t))(g||{});function H(t){const{width:r,height:e}=t.parentElement.getBoundingClientRect(),o={width:300,height:100},a=`0 0 ${r||o.width} ${e||o.height}`,i=document.createElementNS(S,"svg"),u=K();return i.id=u,i.setAttribute("viewBox",a),i.style.width="100%",i.style.height="100%",{svg:i,svgId:u,width:r||o.width,height:e||o.height}}function y(t,r=0){return isNaN(t)?r:t}function R(t){let r=[];for(let e=0;e<t.length;e+=1)r.push(`${y(t[e].x)},${y(t[e].y)} `);return r.join(" ").trim()}function P(t){if(t.length<1)return"0,0";const r=t.length-1,e=[`${y(t[0].x)},${y(t[0].y)}`],o=[],a=[],i=[],u=[];for(let n=0;n<r;n+=1)o[n]=t[n+1].x-t[n].x,a[n]=t[n+1].y-t[n].y,i[n]=a[n]/o[n];u[0]=i[0],u[r]=i[r-1];for(let n=1;n<r;n+=1)if(i[n-1]*i[n]<=0)u[n]=0;else{const d=2*i[n-1]*i[n]/(i[n-1]+i[n]);u[n]=d}for(let n=0;n<r;n+=1){const d=t[n].x,m=t[n].y,c=t[n+1].x,v=t[n+1].y,E=u[n],L=u[n+1],$=d+(c-d)/3,N=m+E*(c-d)/3,I=c-(c-d)/3,p=v-L*(c-d)/3;e.push(`C ${y($)},${y(N)} ${y(I)},${y(p)} ${y(c)},${y(v)}`)}return e.join(" ")}function G(t,r=1e3){const e=t.getTotalLength();t.style.strokeDasharray=String(e),t.style.strokeDashoffset=String(e),t.getBoundingClientRect(),t.style.transition=`stroke-dashoffset ${r}ms ease-in-out`,t.style.strokeDashoffset="0",t.addEventListener("transitionend",function o(){t.style.transition="",t.removeEventListener("transitionend",o)})}function U(t,r,e=1e3){const o=r.getBBox(),a=o.width,i=document.createElementNS("http://www.w3.org/2000/svg","clipPath"),u="clip-"+Math.random().toString(36).substr(2,9);i.setAttribute("id",u);const n=document.createElementNS("http://www.w3.org/2000/svg","rect");n.setAttribute("x",o.x.toString()),n.setAttribute("y",o.y.toString()),n.setAttribute("width","0"),n.setAttribute("height",o.height.toString()),i.appendChild(n);let d=t.querySelector("defs");d||(d=document.createElementNS("http://www.w3.org/2000/svg","defs"),t.insertBefore(d,t.firstChild)),d.appendChild(i),r.setAttribute("clip-path",`url(#${u})`),n.style.transition=`width ${e}ms ease-out`,n.getBoundingClientRect(),n.setAttribute("width",a.toString()),n.addEventListener("transitionend",function m(){r.removeAttribute("clip-path"),i.parentNode&&i.parentNode.removeChild(i),n.removeEventListener("transitionend",m)})}function W(){return document.querySelectorAll(".tiny-spark")}function D(t,r){return Object.keys(t.dataset).includes(r)}function f(t,r,e){return D(t,r)?t.dataset[r]:e}function j(t,r){const e=new MutationObserver(o=>{for(const a of o)if(a.type==="attributes"&&a.attributeName&&Object.values(M).includes(a.attributeName)){r();break}});return e.observe(t,{attributes:!0}),e}function q(t){if(!t)return{color:"#1A1A1A",backgroundColor:"#FFFFFF"};const r=window.getComputedStyle(t),e=r.getPropertyValue("color")||"#1A1A1A",o=r.getPropertyValue("background-color"),a=r.getPropertyValue("background");return{color:e,backgroundColor:o||a||"#FFFFFF"}}function K(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(t){const r=Math.random()*16|0;return(t=="x"?r:r&3|8).toString(16)})}function X(t){const r=t.getAttribute("data-set");if(!r)return[];try{const e=JSON.parse(r);return Array.isArray(e)&&e.every(o=>typeof o=="number"||[null,void 0].includes(o))?e:(console.warn("data-set is not an array of numbers."),[])}catch(e){return console.error("Error parsing data-set:",e),[]}}function Y(t){const r=t.getAttribute("data-dates");if(!r)return[];try{const e=JSON.parse(r);return Array.isArray(e)&&e.every(o=>typeof o=="string")?e:(console.warn("data-dates is not an array of strings"),[])}catch(e){return console.error("Error parsing data-dates",e),[]}}function F(t){return{min:Math.min(...t),max:Math.max(...t)}}function V(){return new Promise(t=>setTimeout(t,0))}function J(t,r){const e=String(f(t,g.NUMBER_LOCALE,navigator.language||"en-US")),o=Number(String(f(t,g.NUMBER_ROUNDING,0)));return r.toLocaleString(e,{useGrouping:!0,minimumFractionDigits:o,maximumFractionDigits:o})}function Z(t,r,e){if(!t.createSVGPoint||!t.getScreenCTM)throw new Error("Your browser does not support SVG coordinate transformation.");const o=t.getScreenCTM();if(!o)throw new Error("Cannot obtain the screen CTM.");const a=t.createSVGPoint();a.x=r,a.y=e;const i=a.matrixTransform(o);return{x:i.x,y:i.y}}function B(t,r,e,o,a){if(z(o),!a)return;const{x:i,y:u}=Z(t,e.x,e.y),n=document.createElement("div");n.classList.add("tiny-spark-tooltip"),n.setAttribute("id",`tooltip_${o}`),n.style.pointerEvents="none",n.style.position="fixed",n.style.top=u+"px",n.style.left=i+"px",n.style.width="fit-content",n.innerHTML=`
|
|
2
|
+
<div class="tiny-spark-tooltip-content">${e.d?`${e.d}: `:""}${[null,void 0].includes(e.v)?"-":J(r,Number(e.v))}</div>
|
|
3
|
+
`,document.body.appendChild(n),V().then(()=>{const{width:d,height:m}=n.getBoundingClientRect();n.style.left=`${i-d/2}px`,n.style.top=`${u-m-Number(String(Number(f(r,g.PLOT_RADIUS,3))*1.5))}px`})}function z(t){const r=document.getElementById(`tooltip_${t}`);r==null||r.remove()}function Q(t){t.innerHTML=""}function T(t,r){let e=r;Q(t);const{svg:o,svgId:a,width:i,height:u}=H(t),{color:n,backgroundColor:d}=q(t),m={T:12,R:12,B:12,L:12},c={left:m.L,top:m.T,width:i-m.L-m.R,height:u-m.T-m.B,bottom:u-m.B},v=X(t),{min:E}=F(v),L=v.map(l=>[null,void 0].includes(l)?l:l+(E<0?Math.abs(E):0)),{max:$}=F(L),N=c.width/(v.length-1)===1/0?c.width:c.width/(v.length-1),I=Y(t),p=L.map((l,h)=>{const s={w:L.length===1?N/2:0,h:L.length===1?c.height/2:0};return{y:(1-(l||0)/$)*c.height+m.T+s.h,x:c.left+N*h+s.w,v:l,d:I[h]||null}}),C=[...p].filter(({v:l})=>![null,void 0].includes(l)),w=document.createElementNS(S,"path");w.classList.add("tiny-spark-line-path"),!t.dataset.curve||t.dataset.curve==="true"?w.setAttribute("d",`M ${P(C)}`):w.setAttribute("d",`M ${R(C)}`),w.setAttribute("fill","none"),w.setAttribute("stroke",String(f(t,g.LINE_COLOR,n))),w.setAttribute("stroke-width",String(f(t,g.LINE_THICKNESS,2))),w.setAttribute("stroke-linecap","round");const O=document.createElementNS(S,"path");O.classList.add("tiny-spark-line-area"),p.length&&(!t.dataset.curve||t.dataset.curve==="true"?O.setAttribute("d",`M ${C[0].x},${c.bottom} ${P(C)} L ${C.at(-1).x},${c.bottom} Z`):O.setAttribute("d",`M ${C[0].x},${c.bottom} ${R(C)} L ${C.at(-1).x},${c.bottom} Z`)),O.setAttribute("fill",String(f(t,g.AREA_COLOR,"transparent"))),p.length>1&&(o.appendChild(O),o.appendChild(w));const et=t.getAttribute("data-animation"),k=[];p.forEach((l,h)=>{const s=document.createElementNS(S,"line");s.classList.add("tiny-spark-indicator"),s.setAttribute("id",`indicator_${a}_${h}`),s.setAttribute("x1",String(c.left+(p.length===1?c.width/2:h*N))),s.setAttribute("x2",String(c.left+(p.length===1?c.width/2:h*N))),s.setAttribute("y1",String(c.top)),s.setAttribute("y2",String(c.bottom)),s.setAttribute("stroke",String(f(t,g.INDICATOR_COLOR,"#1A1A1A"))),s.setAttribute("stroke-width",String(f(t,g.INDICATOR_WIDTH,"1"))),s.setAttribute("stroke-linecap","round"),s.style.pointerEvents="none",s.style.opacity="0",k.push(s),o.appendChild(s)});let _=[];Number(String(f(t,g.PLOT_RADIUS,0)))>0&&p.forEach(({x:l,y:h,v:s},x)=>{if(![null,void 0].includes(s)){const b=document.createElementNS(S,"circle");b.classList.add("tiny-spark-datapoint-circle"),b.classList.add(`circle-${a}`),b.setAttribute("id",`circle_${a}_${x}`),b.setAttribute("cx",String(l)),b.setAttribute("cy",String(h)),b.setAttribute("r",String(f(t,g.PLOT_RADIUS,3))),b.setAttribute("fill",String(f(t,g.PLOT_COLOR,String(f(t,"lineColor",n))))),b.setAttribute("stroke",d),b.style.transition=`opacity ${x*(1e3*2/p.length)}ms ease-in`,b.style.opacity="0",o.appendChild(b),_.push(b)}}),p.forEach((l,h)=>{const s=document.createElementNS(S,"rect");s.classList.add("tiny-spark-tooltip-trap"),s.setAttribute("x",`${p.length===1?0:c.left+h*N-N/2}`),s.setAttribute("y",`${c.top}`),s.setAttribute("height",`${c.height}`),s.setAttribute("width",`${N}`),s.setAttribute("fill","transparent"),s.addEventListener("mouseenter",()=>{B(o,t,l,a,!0);const x=document.getElementById(`circle_${a}_${h}`);x==null||x.setAttribute("r",String(Number(f(t,g.PLOT_RADIUS,3))*1.5)),k[h].style.opacity="1"}),s.addEventListener("mouseout",()=>{B(o,t,l,a,!1);const x=document.getElementById(`circle_${a}_${h}`);x==null||x.setAttribute("r",String(Number(f(t,g.PLOT_RADIUS,3)))),k.forEach(b=>b.style.opacity="0")}),o.appendChild(s)}),et==="true"&&e?V().then(()=>{_.forEach(l=>{l.style.opacity="1"}),G(w),U(o,O)}):_.forEach(l=>{l.style.opacity="1"}),t.appendChild(o)}(function(){window.addEventListener("load",()=>{const r=W();r.length&&Array.from(r).forEach(e=>{tt(e),e.__renderCount=0,A(e),j(e,()=>A(e)),new ResizeObserver(i=>{i.forEach(u=>{A(e)})}).observe(e.parentElement),new MutationObserver(i=>{i.forEach(u=>{u.type==="attributes"&&u.attributeName&&u.attributeName.startsWith("data-")&&A(e)})}).observe(e,{attributes:!0})})})})();function A(t){t.__renderCount+=1,D(t,"set")&&T(t,t.__renderCount<3)}function tt(t){t.dataset.set||console.error(`Tiny-spark exception:
|
|
4
|
+
|
|
5
|
+
[data-set] data attribute is missing.
|
|
6
|
+
Provide an array of numbers, for example:
|
|
7
|
+
|
|
8
|
+
data-set="[1, 2, 3]"`)}});
|
package/dist/vite.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
package/index.html
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Vite + TS</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
|
|
12
|
+
<!-- <div
|
|
13
|
+
data-line
|
|
14
|
+
data-curve="true"
|
|
15
|
+
data-animation="true"
|
|
16
|
+
data-line-color="#4A4A4A"
|
|
17
|
+
data-area-color="#1A1A1A10"
|
|
18
|
+
data-line-thickness="4"
|
|
19
|
+
data-responsive
|
|
20
|
+
data-plot-color="#2A2A2A"
|
|
21
|
+
data-plot-radius="3"
|
|
22
|
+
data-number-locale="en-US"
|
|
23
|
+
data-number-rounding="2"
|
|
24
|
+
data-indicator-color="#1A1A1A"
|
|
25
|
+
data-indicator-width="1"
|
|
26
|
+
data-set="[1, 2, 3]"
|
|
27
|
+
></div> -->
|
|
28
|
+
|
|
29
|
+
<script type="module" src="/src/main.ts"></script>
|
|
30
|
+
<!-- <script type="module" src="dist/tiny-spark.umd.js"></script> -->
|
|
31
|
+
</body>
|
|
32
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tiny-spark",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.1.0-alpha.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "An elegant, reactive and responsive sparkline chart solution without dependency.",
|
|
7
|
+
"author": "Alec Lloyd Probert",
|
|
8
|
+
"main": "dist/tiny-spark.umd.js",
|
|
9
|
+
"module": "dist/tiny-spark.es.js",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"sparkline",
|
|
12
|
+
"chart",
|
|
13
|
+
"mini chart",
|
|
14
|
+
"line"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/graphieros/tiny-spark.git"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"dev": "vite",
|
|
23
|
+
"build": "tsc && vite build"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"typescript": "~5.7.2",
|
|
27
|
+
"vite": "^6.2.3"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/public/vite.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { TINY_SPARK } from "../types"
|
|
2
|
+
import { getCharts, observe, createLineChart, hasDataset } from "./lib"
|
|
3
|
+
|
|
4
|
+
(function MAIN() {
|
|
5
|
+
|
|
6
|
+
window.addEventListener('load', () => {
|
|
7
|
+
const charts = getCharts();
|
|
8
|
+
if (!charts.length) return;
|
|
9
|
+
|
|
10
|
+
Array.from(charts).forEach((chart) => {
|
|
11
|
+
CHECK(chart as TINY_SPARK);
|
|
12
|
+
(chart as TINY_SPARK).__renderCount = 0;
|
|
13
|
+
RENDER(chart as TINY_SPARK);
|
|
14
|
+
observe(chart as TINY_SPARK, () => RENDER(chart as TINY_SPARK));
|
|
15
|
+
|
|
16
|
+
const spy = new ResizeObserver((entries) => {
|
|
17
|
+
entries.forEach(_ => {
|
|
18
|
+
RENDER(chart as TINY_SPARK);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
spy.observe(chart.parentElement!);
|
|
23
|
+
|
|
24
|
+
const mutation = new MutationObserver((mutations) => {
|
|
25
|
+
mutations.forEach(mutation => {
|
|
26
|
+
if (
|
|
27
|
+
mutation.type === 'attributes' &&
|
|
28
|
+
mutation.attributeName &&
|
|
29
|
+
mutation.attributeName.startsWith('data-')
|
|
30
|
+
) {
|
|
31
|
+
RENDER(chart as TINY_SPARK);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
mutation.observe(chart, { attributes: true });
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}());
|
|
39
|
+
|
|
40
|
+
function RENDER(chart: TINY_SPARK) {
|
|
41
|
+
chart.__renderCount += 1;
|
|
42
|
+
hasDataset(chart, 'set') && createLineChart(chart, chart.__renderCount < 3);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function CHECK(chart: TINY_SPARK) {
|
|
46
|
+
if (!chart.dataset.set) {
|
|
47
|
+
console.error(`Tiny-spark exception:\n\n [data-set] data attribute is missing.\n Provide an array of numbers, for example:\n\n data-set="[1, 2, 3]"`)
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/lib.ts
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { CHART_TYPE, DATA, TINY_SPARK, POINT, XMLNS, DATA_ATTRIBUTE, ANIMATION_DURATION } from "../types"
|
|
2
|
+
import { animateAreaProgressively, animatePath, createSmoothPath, createStraightPath, SVG } from "./svg";
|
|
3
|
+
|
|
4
|
+
export function getCharts() {
|
|
5
|
+
const charts = document.querySelectorAll('.tiny-spark')
|
|
6
|
+
return charts
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isChartOfType(element: TINY_SPARK, type: CHART_TYPE) {
|
|
10
|
+
const attrs = Object.keys(element.dataset)
|
|
11
|
+
return attrs.includes(type)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function hasDataset(element: TINY_SPARK, name: string) {
|
|
15
|
+
const attrs = Object.keys(element.dataset)
|
|
16
|
+
return attrs.includes(name)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getDatasetValue(element: TINY_SPARK, name: string, fallback: number | string) {
|
|
20
|
+
if (!hasDataset(element, name)) return fallback;
|
|
21
|
+
return element.dataset[name]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function observe(element: TINY_SPARK, render: () => void) {
|
|
25
|
+
const observer = new MutationObserver((mutationsList) => {
|
|
26
|
+
for (const mutation of mutationsList) {
|
|
27
|
+
if (
|
|
28
|
+
mutation.type === 'attributes' &&
|
|
29
|
+
mutation.attributeName &&
|
|
30
|
+
Object.values(DATA).includes(mutation.attributeName as DATA)
|
|
31
|
+
) {
|
|
32
|
+
render();
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
observer.observe(element, { attributes: true });
|
|
39
|
+
return observer;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getElementColors(element: TINY_SPARK) {
|
|
43
|
+
if (!element) return {
|
|
44
|
+
color: '#1A1A1A',
|
|
45
|
+
backgroundColor: '#FFFFFF'
|
|
46
|
+
};
|
|
47
|
+
const computedStyle = window.getComputedStyle(element);
|
|
48
|
+
const color = computedStyle.getPropertyValue('color') || '#1A1A1A';
|
|
49
|
+
const backgroundColor = computedStyle.getPropertyValue('background-color');
|
|
50
|
+
const background = computedStyle.getPropertyValue('background');
|
|
51
|
+
const bg = backgroundColor || background || '#FFFFFF';
|
|
52
|
+
return { color, backgroundColor: bg };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createUid() {
|
|
56
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
|
|
57
|
+
.replace(/[xy]/g, function (c) {
|
|
58
|
+
const r = Math.random() * 16 | 0,
|
|
59
|
+
v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
60
|
+
return v.toString(16);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function parseDataset(chart: TINY_SPARK) {
|
|
65
|
+
const dataSetStr = chart.getAttribute('data-set');
|
|
66
|
+
if (!dataSetStr) return [];
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(dataSetStr);
|
|
69
|
+
if (Array.isArray(parsed) && parsed.every(item => typeof item === 'number' || [null, undefined].includes(item))) {
|
|
70
|
+
return parsed;
|
|
71
|
+
}
|
|
72
|
+
console.warn('data-set is not an array of numbers.');
|
|
73
|
+
return [];
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error('Error parsing data-set:', error);
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getDates(chart: TINY_SPARK) {
|
|
81
|
+
const dates = chart.getAttribute('data-dates');
|
|
82
|
+
if (!dates) return []
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(dates);
|
|
85
|
+
if (Array.isArray(parsed) && parsed.every(item => typeof item === 'string')) {
|
|
86
|
+
return parsed;
|
|
87
|
+
}
|
|
88
|
+
console.warn('data-dates is not an array of strings');
|
|
89
|
+
return [];
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error('Error parsing data-dates', error);
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function minMax(dataset: number[]) {
|
|
97
|
+
return {
|
|
98
|
+
min: Math.min(...dataset),
|
|
99
|
+
max: Math.max(...dataset)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function nextTick() {
|
|
104
|
+
return new Promise(resolve => setTimeout(resolve, 0));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function localeNum(chart: TINY_SPARK, num: number) {
|
|
108
|
+
const locale = String(getDatasetValue(chart, DATA_ATTRIBUTE.NUMBER_LOCALE, navigator.language || 'en-US'));
|
|
109
|
+
const rounding = Number(String(getDatasetValue(chart, DATA_ATTRIBUTE.NUMBER_ROUNDING, 0)));
|
|
110
|
+
return num.toLocaleString(locale, {
|
|
111
|
+
useGrouping: true,
|
|
112
|
+
minimumFractionDigits: rounding,
|
|
113
|
+
maximumFractionDigits: rounding
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function domPlot(svg: SVGSVGElement, svgX: number, svgY: number) {
|
|
118
|
+
if (!svg.createSVGPoint || !svg.getScreenCTM) {
|
|
119
|
+
throw new Error("Your browser does not support SVG coordinate transformation.");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const screenCTM = svg.getScreenCTM();
|
|
123
|
+
if (!screenCTM) {
|
|
124
|
+
throw new Error("Cannot obtain the screen CTM.");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const point = svg.createSVGPoint();
|
|
128
|
+
point.x = svgX;
|
|
129
|
+
point.y = svgY;
|
|
130
|
+
|
|
131
|
+
const domPoint = point.matrixTransform(screenCTM);
|
|
132
|
+
|
|
133
|
+
return { x: domPoint.x, y: domPoint.y };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function tooltip(svg: SVGSVGElement, chart: TINY_SPARK, point: POINT, id: string, show: boolean) {
|
|
137
|
+
nukeTooltip(id);
|
|
138
|
+
if (!show) return;
|
|
139
|
+
const { x, y } = domPlot(svg, point.x, point.y);
|
|
140
|
+
const tool = document.createElement('div');
|
|
141
|
+
tool.classList.add('tiny-spark-tooltip');
|
|
142
|
+
tool.setAttribute('id', `tooltip_${id}`);
|
|
143
|
+
tool.style.pointerEvents = 'none';
|
|
144
|
+
tool.style.position = 'fixed';
|
|
145
|
+
tool.style.top = y + 'px';
|
|
146
|
+
tool.style.left = x + 'px';
|
|
147
|
+
tool.style.width = 'fit-content';
|
|
148
|
+
tool.innerHTML = `
|
|
149
|
+
<div class="tiny-spark-tooltip-content">${!point.d ? '' : `${point.d}: `}${[null, undefined].includes(point.v as any) ? '-' : localeNum(chart, Number(point.v))}</div>
|
|
150
|
+
`
|
|
151
|
+
document.body.appendChild(tool);
|
|
152
|
+
nextTick().then(() => {
|
|
153
|
+
const { width, height } = tool.getBoundingClientRect()
|
|
154
|
+
tool.style.left = `${x - width / 2}px`;
|
|
155
|
+
tool.style.top = `${y - height - Number(String(Number(getDatasetValue(chart, DATA_ATTRIBUTE.PLOT_RADIUS, 3)) * 1.5))}px`
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function nukeTooltip(id: string) {
|
|
160
|
+
const t = document.getElementById(`tooltip_${id}`);
|
|
161
|
+
t?.remove();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
/////////////////////////////////////////////////////
|
|
166
|
+
|
|
167
|
+
function clear(chart: TINY_SPARK) {
|
|
168
|
+
chart.innerHTML = ''
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function createLineChart(chart: TINY_SPARK, firstTime: boolean) {
|
|
172
|
+
let animate = firstTime;
|
|
173
|
+
clear(chart);
|
|
174
|
+
const { svg, svgId, width, height } = SVG(chart);
|
|
175
|
+
const { color, backgroundColor } = getElementColors(chart)
|
|
176
|
+
|
|
177
|
+
const padding = { T: 12, R: 12, B: 12, L: 12 };
|
|
178
|
+
|
|
179
|
+
// DRAWING AREA
|
|
180
|
+
const area = {
|
|
181
|
+
left: padding.L,
|
|
182
|
+
top: padding.T,
|
|
183
|
+
width: width - padding.L - padding.R,
|
|
184
|
+
height: height - padding.T - padding.B,
|
|
185
|
+
bottom: height - padding.B,
|
|
186
|
+
right: width - padding.R
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const dataset = parseDataset(chart);
|
|
190
|
+
const { min: MIN } = minMax(dataset);
|
|
191
|
+
const positiveDataset = dataset.map(d => {
|
|
192
|
+
return [null, undefined].includes(d) ? d : d + (MIN < 0 ? Math.abs(MIN) : 0)
|
|
193
|
+
});
|
|
194
|
+
const { max } = minMax(positiveDataset);
|
|
195
|
+
const slot = area.width / (dataset.length - 1) === Infinity ? area.width : area.width / (dataset.length - 1);
|
|
196
|
+
|
|
197
|
+
const dates = getDates(chart);
|
|
198
|
+
|
|
199
|
+
// DATAPOINTS
|
|
200
|
+
const allPoints = positiveDataset.map((d, i) => {
|
|
201
|
+
const uniqueCase = {
|
|
202
|
+
w: positiveDataset.length === 1 ? slot / 2 : 0,
|
|
203
|
+
h: positiveDataset.length === 1 ? area.height / 2 : 0
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
y: ((1 - ((d || 0) / max)) * area.height) + padding.T + uniqueCase.h,
|
|
207
|
+
x: area.left + ((slot * i)) + uniqueCase.w,
|
|
208
|
+
v: d,
|
|
209
|
+
d: dates[i] || null
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const points = [...allPoints].filter(({ v }) => ![null, undefined].includes(v));
|
|
214
|
+
|
|
215
|
+
// PATH & AREA
|
|
216
|
+
const path = document.createElementNS(XMLNS, 'path');
|
|
217
|
+
path.classList.add('tiny-spark-line-path');
|
|
218
|
+
|
|
219
|
+
if (!chart.dataset.curve || chart.dataset.curve === 'true') {
|
|
220
|
+
path.setAttribute('d', `M ${createSmoothPath(points)}`);
|
|
221
|
+
} else {
|
|
222
|
+
path.setAttribute('d', `M ${createStraightPath(points)}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
path.setAttribute('fill', 'none');
|
|
226
|
+
path.setAttribute('stroke', String(getDatasetValue(chart, DATA_ATTRIBUTE.LINE_COLOR, color)));
|
|
227
|
+
path.setAttribute('stroke-width', String(getDatasetValue(chart, DATA_ATTRIBUTE.LINE_THICKNESS, 2)));
|
|
228
|
+
path.setAttribute('stroke-linecap', 'round');
|
|
229
|
+
|
|
230
|
+
const pathArea = document.createElementNS(XMLNS, 'path');
|
|
231
|
+
pathArea.classList.add('tiny-spark-line-area');
|
|
232
|
+
|
|
233
|
+
if (allPoints.length) {
|
|
234
|
+
if (!chart.dataset.curve || chart.dataset.curve === 'true') {
|
|
235
|
+
pathArea.setAttribute('d', `M ${points[0].x},${area.bottom} ${createSmoothPath(points)} L ${points.at(-1)!.x},${area.bottom} Z`);
|
|
236
|
+
} else {
|
|
237
|
+
pathArea.setAttribute('d', `M ${points[0].x},${area.bottom} ${createStraightPath(points)} L ${points.at(-1)!.x},${area.bottom} Z`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
pathArea.setAttribute('fill', String(getDatasetValue(chart, DATA_ATTRIBUTE.AREA_COLOR, 'transparent')));
|
|
242
|
+
|
|
243
|
+
if (allPoints.length > 1) {
|
|
244
|
+
svg.appendChild(pathArea);
|
|
245
|
+
svg.appendChild(path);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
const animation = chart.getAttribute('data-animation');
|
|
250
|
+
|
|
251
|
+
const indicators: SVGLineElement[] = [];
|
|
252
|
+
|
|
253
|
+
// VERTICAL INDICATOR
|
|
254
|
+
allPoints.forEach((_, i) => {
|
|
255
|
+
const indicator = document.createElementNS(XMLNS, 'line');
|
|
256
|
+
indicator.classList.add('tiny-spark-indicator');
|
|
257
|
+
indicator.setAttribute('id', `indicator_${svgId}_${i}`);
|
|
258
|
+
indicator.setAttribute('x1', String(area.left + (allPoints.length === 1 ? area.width / 2 : (i * slot))));
|
|
259
|
+
indicator.setAttribute('x2', String(area.left + (allPoints.length === 1 ? area.width / 2 : (i * slot))));
|
|
260
|
+
indicator.setAttribute('y1', String(area.top));
|
|
261
|
+
indicator.setAttribute('y2', String(area.bottom));
|
|
262
|
+
indicator.setAttribute('stroke', String(getDatasetValue(chart, DATA_ATTRIBUTE.INDICATOR_COLOR, '#1A1A1A')));
|
|
263
|
+
indicator.setAttribute('stroke-width', String(getDatasetValue(chart, DATA_ATTRIBUTE.INDICATOR_WIDTH, '1')));
|
|
264
|
+
indicator.setAttribute('stroke-linecap', 'round');
|
|
265
|
+
indicator.style.pointerEvents = 'none';
|
|
266
|
+
indicator.style.opacity = '0';
|
|
267
|
+
indicators.push(indicator);
|
|
268
|
+
svg.appendChild(indicator);
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
let plots: SVGCircleElement[] = [];
|
|
272
|
+
|
|
273
|
+
// PLOTS
|
|
274
|
+
if (Number(String(getDatasetValue(chart, DATA_ATTRIBUTE.PLOT_RADIUS, 0))) > 0) {
|
|
275
|
+
allPoints.forEach(({ x, y, v }, i) => {
|
|
276
|
+
if (![null, undefined].includes(v)) {
|
|
277
|
+
const circle = document.createElementNS(XMLNS, 'circle');
|
|
278
|
+
circle.classList.add('tiny-spark-datapoint-circle');
|
|
279
|
+
circle.classList.add(`circle-${svgId}`);
|
|
280
|
+
circle.setAttribute('id', `circle_${svgId}_${i}`);
|
|
281
|
+
circle.setAttribute('cx', String(x));
|
|
282
|
+
circle.setAttribute('cy', String(y));
|
|
283
|
+
circle.setAttribute('r', String(getDatasetValue(chart, DATA_ATTRIBUTE.PLOT_RADIUS, 3)));
|
|
284
|
+
circle.setAttribute('fill', String(getDatasetValue(chart, DATA_ATTRIBUTE.PLOT_COLOR, String(getDatasetValue(chart, 'lineColor', color)))));
|
|
285
|
+
circle.setAttribute('stroke', backgroundColor);
|
|
286
|
+
circle.style.transition = `opacity ${i * ((ANIMATION_DURATION * 2) / allPoints.length)}ms ease-in`;
|
|
287
|
+
circle.style.opacity = '0';
|
|
288
|
+
svg.appendChild(circle);
|
|
289
|
+
plots.push(circle);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// TOOLTIP TRAPS
|
|
295
|
+
allPoints.forEach((point, i) => {
|
|
296
|
+
const trap = document.createElementNS(XMLNS, 'rect');
|
|
297
|
+
trap.classList.add('tiny-spark-tooltip-trap');
|
|
298
|
+
trap.setAttribute('x', `${allPoints.length === 1 ? 0 : area.left + i * slot - slot / 2}`);
|
|
299
|
+
trap.setAttribute('y', `${area.top}`);
|
|
300
|
+
trap.setAttribute('height', `${area.height}`);
|
|
301
|
+
trap.setAttribute('width', `${slot}`);
|
|
302
|
+
trap.setAttribute('fill', 'transparent');
|
|
303
|
+
trap.addEventListener('mouseenter', () => {
|
|
304
|
+
tooltip(svg, chart, point, svgId, true);
|
|
305
|
+
const circle = document.getElementById(`circle_${svgId}_${i}`);
|
|
306
|
+
circle?.setAttribute('r', String(Number(getDatasetValue(chart, DATA_ATTRIBUTE.PLOT_RADIUS, 3)) * 1.5))
|
|
307
|
+
indicators[i].style.opacity = '1';
|
|
308
|
+
});
|
|
309
|
+
trap.addEventListener('mouseout', () => {
|
|
310
|
+
tooltip(svg, chart, point, svgId, false);
|
|
311
|
+
const circle = document.getElementById(`circle_${svgId}_${i}`);
|
|
312
|
+
circle?.setAttribute('r', String(Number(getDatasetValue(chart, DATA_ATTRIBUTE.PLOT_RADIUS, 3))))
|
|
313
|
+
indicators.forEach(indicator => indicator.style.opacity = '0');
|
|
314
|
+
});
|
|
315
|
+
svg.appendChild(trap);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
if (animation === 'true' && animate) {
|
|
320
|
+
nextTick().then(() => {
|
|
321
|
+
plots.forEach(circle => {
|
|
322
|
+
circle.style.opacity = '1'
|
|
323
|
+
})
|
|
324
|
+
animatePath(path);
|
|
325
|
+
animateAreaProgressively(svg as unknown as TINY_SPARK, pathArea);
|
|
326
|
+
})
|
|
327
|
+
} else {
|
|
328
|
+
plots.forEach(circle => {
|
|
329
|
+
circle.style.opacity = '1'
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
chart.appendChild(svg);
|
|
334
|
+
}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import './style.css'
|
|
2
|
+
|
|
3
|
+
function makeDs(n: number) {
|
|
4
|
+
let arr = [];
|
|
5
|
+
for(let i = 0; i < n; i += 1) {
|
|
6
|
+
arr.push(Math.random());
|
|
7
|
+
}
|
|
8
|
+
return arr.toString();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
|
|
12
|
+
<div style="width:100%">
|
|
13
|
+
<div
|
|
14
|
+
class="tiny-spark"
|
|
15
|
+
data-curve="true"
|
|
16
|
+
data-animation="true"
|
|
17
|
+
data-line-color="#4A4A4A"
|
|
18
|
+
data-area-color="#1A1A1A10"
|
|
19
|
+
data-line-thickness="3"
|
|
20
|
+
data-responsive
|
|
21
|
+
data-plot-color="#2A2A2A"
|
|
22
|
+
data-plot-radius="3"
|
|
23
|
+
data-number-locale="en-US"
|
|
24
|
+
data-number-rounding="2"
|
|
25
|
+
data-indicator-color="#1A1A1A"
|
|
26
|
+
data-indicator-width="1"
|
|
27
|
+
data-set="[${makeDs(6)}]"
|
|
28
|
+
data-dates='["jan", "feb", "mar", "apr", "may", "jun"]'
|
|
29
|
+
>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
`
|
|
33
|
+
import './index'
|
package/src/style.css
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
3
|
+
line-height: 1.5;
|
|
4
|
+
font-weight: 400;
|
|
5
|
+
|
|
6
|
+
color-scheme: light dark;
|
|
7
|
+
color: rgba(255, 255, 255, 0.87);
|
|
8
|
+
background-color: #242424;
|
|
9
|
+
|
|
10
|
+
font-synthesis: none;
|
|
11
|
+
text-rendering: optimizeLegibility;
|
|
12
|
+
-webkit-font-smoothing: antialiased;
|
|
13
|
+
-moz-osx-font-smoothing: grayscale;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
a {
|
|
17
|
+
font-weight: 500;
|
|
18
|
+
color: #646cff;
|
|
19
|
+
text-decoration: inherit;
|
|
20
|
+
}
|
|
21
|
+
a:hover {
|
|
22
|
+
color: #535bf2;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
body {
|
|
26
|
+
margin: 0;
|
|
27
|
+
display: flex;
|
|
28
|
+
place-items: center;
|
|
29
|
+
min-height: 100vh;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
h1 {
|
|
33
|
+
font-size: 3.2em;
|
|
34
|
+
line-height: 1.1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#app {
|
|
38
|
+
max-width: 1280px;
|
|
39
|
+
margin: 0 auto;
|
|
40
|
+
padding: 2rem;
|
|
41
|
+
text-align: center;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.logo {
|
|
45
|
+
height: 6em;
|
|
46
|
+
padding: 1.5em;
|
|
47
|
+
will-change: filter;
|
|
48
|
+
transition: filter 300ms;
|
|
49
|
+
}
|
|
50
|
+
.logo:hover {
|
|
51
|
+
filter: drop-shadow(0 0 2em #646cffaa);
|
|
52
|
+
}
|
|
53
|
+
.logo.vanilla:hover {
|
|
54
|
+
filter: drop-shadow(0 0 2em #3178c6aa);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.card {
|
|
58
|
+
padding: 2em;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.read-the-docs {
|
|
62
|
+
color: #888;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
button {
|
|
66
|
+
border-radius: 8px;
|
|
67
|
+
border: 1px solid transparent;
|
|
68
|
+
padding: 0.6em 1.2em;
|
|
69
|
+
font-size: 1em;
|
|
70
|
+
font-weight: 500;
|
|
71
|
+
font-family: inherit;
|
|
72
|
+
background-color: #1a1a1a;
|
|
73
|
+
cursor: pointer;
|
|
74
|
+
transition: border-color 0.25s;
|
|
75
|
+
}
|
|
76
|
+
button:hover {
|
|
77
|
+
border-color: #646cff;
|
|
78
|
+
}
|
|
79
|
+
button:focus,
|
|
80
|
+
button:focus-visible {
|
|
81
|
+
outline: 4px auto -webkit-focus-ring-color;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@media (prefers-color-scheme: light) {
|
|
85
|
+
:root {
|
|
86
|
+
color: #213547;
|
|
87
|
+
background-color: #ffffff;
|
|
88
|
+
}
|
|
89
|
+
a:hover {
|
|
90
|
+
color: #747bff;
|
|
91
|
+
}
|
|
92
|
+
button {
|
|
93
|
+
background-color: #f9f9f9;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.tiny-spark {
|
|
98
|
+
background-color: #9A9A9A;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#app {
|
|
102
|
+
width: 100%;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.tiny-spark-tooltip {
|
|
106
|
+
border-radius: 3px;
|
|
107
|
+
padding: 0 12px;
|
|
108
|
+
font-size: 10px;
|
|
109
|
+
/* display: none; */
|
|
110
|
+
/* background: red; */
|
|
111
|
+
}
|
package/src/svg.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { TINY_SPARK, POINT, XMLNS, ANIMATION_DURATION } from "../types";
|
|
2
|
+
import { createUid } from "./lib";
|
|
3
|
+
|
|
4
|
+
export function SVG(chart: TINY_SPARK) {
|
|
5
|
+
const { width, height } = chart.parentElement!.getBoundingClientRect();
|
|
6
|
+
const fallback = { width: 300, height: 100 };
|
|
7
|
+
const viewBox = `0 0 ${width || fallback.width} ${height || fallback.height}`;
|
|
8
|
+
|
|
9
|
+
const svg = document.createElementNS(XMLNS, 'svg');
|
|
10
|
+
const id = createUid();
|
|
11
|
+
svg.id = id;
|
|
12
|
+
svg.setAttribute('viewBox', viewBox);
|
|
13
|
+
svg.style.width = '100%';
|
|
14
|
+
svg.style.height = '100%';
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
svg,
|
|
18
|
+
svgId: id,
|
|
19
|
+
width: width || fallback.width,
|
|
20
|
+
height: height || fallback.height
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function checkNaN(val: number, fallback = 0) {
|
|
25
|
+
if (isNaN(val)) {
|
|
26
|
+
return fallback
|
|
27
|
+
} else {
|
|
28
|
+
return val
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createStraightPath(points: POINT[]) {
|
|
33
|
+
let arr = [];
|
|
34
|
+
for (let i = 0; i < points.length; i += 1) {
|
|
35
|
+
arr.push(`${checkNaN(points[i].x)},${checkNaN(points[i].y)} `)
|
|
36
|
+
}
|
|
37
|
+
return arr.join(' ').trim()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createSmoothPath(points: POINT[]) {
|
|
41
|
+
if (points.length < 1) return '0,0';
|
|
42
|
+
|
|
43
|
+
const n = points.length - 1;
|
|
44
|
+
const path = [`${checkNaN(points[0].x)},${checkNaN(points[0].y)}`];
|
|
45
|
+
const dx = [], dy = [], slopes = [], tangents = [];
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < n; i += 1) {
|
|
48
|
+
dx[i] = points[i + 1].x - points[i].x;
|
|
49
|
+
dy[i] = points[i + 1].y - points[i].y;
|
|
50
|
+
slopes[i] = dy[i] / dx[i];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
tangents[0] = slopes[0];
|
|
54
|
+
tangents[n] = slopes[n - 1];
|
|
55
|
+
|
|
56
|
+
for (let i = 1; i < n; i += 1) {
|
|
57
|
+
if (slopes[i - 1] * slopes[i] <= 0) {
|
|
58
|
+
tangents[i] = 0;
|
|
59
|
+
} else {
|
|
60
|
+
const harmonicMean = (2 * slopes[i - 1] * slopes[i]) / (slopes[i - 1] + slopes[i]);
|
|
61
|
+
tangents[i] = harmonicMean;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < n; i += 1) {
|
|
66
|
+
const x1 = points[i].x;
|
|
67
|
+
const y1 = points[i].y;
|
|
68
|
+
const x2 = points[i + 1].x;
|
|
69
|
+
const y2 = points[i + 1].y;
|
|
70
|
+
const m1 = tangents[i];
|
|
71
|
+
const m2 = tangents[i + 1];
|
|
72
|
+
const controlX1 = x1 + (x2 - x1) / 3;
|
|
73
|
+
const controlY1 = y1 + m1 * (x2 - x1) / 3;
|
|
74
|
+
const controlX2 = x2 - (x2 - x1) / 3;
|
|
75
|
+
const controlY2 = y2 - m2 * (x2 - x1) / 3;
|
|
76
|
+
|
|
77
|
+
path.push(`C ${checkNaN(controlX1)},${checkNaN(controlY1)} ${checkNaN(controlX2)},${checkNaN(controlY2)} ${checkNaN(x2)},${checkNaN(y2)}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return path.join(' ');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function animatePath(path: SVGPathElement, duration = ANIMATION_DURATION) {
|
|
84
|
+
const totalLength = path.getTotalLength();
|
|
85
|
+
path.style.strokeDasharray = String(totalLength);
|
|
86
|
+
path.style.strokeDashoffset = String(totalLength);
|
|
87
|
+
path.getBoundingClientRect();
|
|
88
|
+
path.style.transition = `stroke-dashoffset ${duration}ms ease-in-out`;
|
|
89
|
+
path.style.strokeDashoffset = '0';
|
|
90
|
+
path.addEventListener('transitionend', function handler() {
|
|
91
|
+
path.style.transition = '';
|
|
92
|
+
path.removeEventListener('transitionend', handler);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function animateAreaProgressively(svg: TINY_SPARK, areaElement: SVGPathElement, duration = ANIMATION_DURATION) {
|
|
97
|
+
const bbox = areaElement.getBBox();
|
|
98
|
+
const fullWidth = bbox.width;
|
|
99
|
+
const clipPath = document.createElementNS("http://www.w3.org/2000/svg", "clipPath");
|
|
100
|
+
const clipId = "clip-" + Math.random().toString(36).substr(2, 9);
|
|
101
|
+
clipPath.setAttribute("id", clipId);
|
|
102
|
+
|
|
103
|
+
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
104
|
+
rect.setAttribute("x", bbox.x.toString());
|
|
105
|
+
rect.setAttribute("y", bbox.y.toString());
|
|
106
|
+
rect.setAttribute("width", "0");
|
|
107
|
+
rect.setAttribute("height", bbox.height.toString());
|
|
108
|
+
clipPath.appendChild(rect);
|
|
109
|
+
|
|
110
|
+
let defs = svg.querySelector("defs");
|
|
111
|
+
if (!defs) {
|
|
112
|
+
defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
|
|
113
|
+
svg.insertBefore(defs, svg.firstChild);
|
|
114
|
+
}
|
|
115
|
+
defs.appendChild(clipPath);
|
|
116
|
+
areaElement.setAttribute("clip-path", `url(#${clipId})`);
|
|
117
|
+
rect.style.transition = `width ${duration}ms ease-out`;
|
|
118
|
+
rect.getBoundingClientRect();
|
|
119
|
+
rect.setAttribute("width", fullWidth.toString());
|
|
120
|
+
|
|
121
|
+
rect.addEventListener("transitionend", function handler() {
|
|
122
|
+
areaElement.removeAttribute("clip-path");
|
|
123
|
+
clipPath.parentNode && clipPath.parentNode.removeChild(clipPath);
|
|
124
|
+
rect.removeEventListener("transitionend", handler);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
|
|
9
|
+
/* Bundler mode */
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"moduleDetection": "force",
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
|
|
16
|
+
/* Linting */
|
|
17
|
+
"strict": true,
|
|
18
|
+
"noUnusedLocals": true,
|
|
19
|
+
"noUnusedParameters": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedSideEffectImports": true
|
|
22
|
+
},
|
|
23
|
+
"include": ["src"]
|
|
24
|
+
}
|
package/types/index.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type TINY_SPARK = HTMLDivElement & {
|
|
2
|
+
__renderCount: number
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export const XMLNS = 'http://www.w3.org/2000/svg'
|
|
6
|
+
export const ANIMATION_DURATION = 1000;
|
|
7
|
+
|
|
8
|
+
export enum CHART_TYPE {
|
|
9
|
+
BAR = 'bar',
|
|
10
|
+
LINE = 'line'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export enum CHART {
|
|
14
|
+
BAR = `data-${CHART_TYPE.BAR}`,
|
|
15
|
+
LINE = `data-${CHART_TYPE.LINE}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export enum DATA {
|
|
19
|
+
BAR = CHART.BAR,
|
|
20
|
+
LINE = CHART.LINE,
|
|
21
|
+
SET = 'data-set',
|
|
22
|
+
RESPONSIVE = 'data-responsive'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type POINT = { x: number; y: number, v: number | null | undefined, d: string | null | undefined }
|
|
26
|
+
|
|
27
|
+
export enum DATA_ATTRIBUTE {
|
|
28
|
+
ANIMATION = 'animation',
|
|
29
|
+
AREA_COLOR = 'areaColor',
|
|
30
|
+
CURVE = 'curve',
|
|
31
|
+
DATES = 'dates',
|
|
32
|
+
INDICATOR_COLOR = 'indicatorColor',
|
|
33
|
+
INDICATOR_WIDTH = 'indicatorWidth',
|
|
34
|
+
LINE_COLOR = 'lineColor',
|
|
35
|
+
LINE_THICKNESS = 'lineThickness',
|
|
36
|
+
NUMBER_LOCALE = 'numberLocale',
|
|
37
|
+
NUMBER_ROUNDING = 'numberRounding',
|
|
38
|
+
NUMBER_SHOW_ON = 'numberShowOn',
|
|
39
|
+
PLOT_COLOR = 'plotColor',
|
|
40
|
+
PLOT_RADIUS = 'plotRadius',
|
|
41
|
+
SET = 'set',
|
|
42
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
build: {
|
|
5
|
+
lib: {
|
|
6
|
+
entry: 'src/index.ts',
|
|
7
|
+
name: 'tiny-spark',
|
|
8
|
+
fileName: (format) => `tiny-spark.${format}.js`,
|
|
9
|
+
formats: ['es', 'umd'],
|
|
10
|
+
},
|
|
11
|
+
rollupOptions: {
|
|
12
|
+
external: [],
|
|
13
|
+
output: {
|
|
14
|
+
globals: {}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
})
|