Skip to content

Commit 6ddca18

Browse files
committed
chore(deps): migrate from chartist to d3 (closes #1142)
1 parent b5ada3d commit 6ddca18

File tree

4 files changed

+190
-12
lines changed

4 files changed

+190
-12
lines changed

.github/actions/spelling/expect.txt

-1
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,6 @@ Nixinova
211211
NOASSERTION
212212
nocase
213213
nodeca
214-
nodechartist
215214
nodejs
216215
notoken
217216
octicon

ARCHITECTURE.md

-2
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,6 @@ Below is a list of used packages.
119119
* To parse and handle emojis/[twemojis](https://github.com/twitter/twemoji)
120120
* [jshemas/openGraphScraper](https://github.com/jshemas/openGraphScraper)
121121
* To retrieve open graphs metadata
122-
* [panosoft/node-chartist](https://github.com/panosoft/node-chartist) and [gionkunz/chartist-js](https://github.com/gionkunz/chartist-js)
123-
* To display embed SVG charts
124122
* [rbren/rss-parser](https://github.com/rbren/rss-parser)
125123
* To parse RSS streams
126124
* [Nixinova/Linguist](https://github.com/Nixinova/Linguist)

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@
8484
"color": "^4.2.3",
8585
"gifencoder": "^2.0.1",
8686
"libxmljs2": "^0.31.0",
87-
"node-chartist": "^1.0.5",
8887
"rss": "^1.2.2",
8988
"yargs-parser": "^21.1.1"
9089
}

source/app/metrics/utils.mjs

+190-8
Original file line numberDiff line numberDiff line change
@@ -207,14 +207,6 @@ export function stripemojis(string) {
207207
return string.replace(/[^\p{L}\p{N}\p{P}\p{Z}^$\n]/gu, "")
208208
}
209209

210-
/**Chartist */
211-
export async function chartist() {
212-
const css = `<style data-optimizable="true">${await fs.readFile(paths.join(__module(import.meta.url), "../../../node_modules", "node-chartist/dist/main.css")).catch(_ => "")}</style>`
213-
const {default: nodechartist} = await import(url.pathToFileURL(paths.join(__module(import.meta.url), "../../../node_modules", "/node-chartist/lib/index.js")))
214-
return (await nodechartist(...arguments))
215-
.replace(/class="ct-chart-line">/, `class="ct-chart-line">${css}`)
216-
}
217-
218210
/**Language analyzer (single file) */
219211
export async function language({filename, patch}) {
220212
console.debug(`metrics/language > ${filename}`)
@@ -816,3 +808,193 @@ export class D3node {
816808
return this.element.select("svg").node()?.outerHTML || ""
817809
}
818810
}
811+
812+
/** Graph utilities */
813+
export const Graph = {
814+
/**Timeline graph */
815+
timeline() {
816+
return this.graph("time", ...arguments)
817+
},
818+
/**Line graph */
819+
line() {
820+
return this.graph("line", ...arguments)
821+
},
822+
/**Basic Graph */
823+
graph(type, data, {area = true, points = true, text = true, low = NaN, high = NaN, match = null, labels = null, width = 480, height = 315, ticks = 0} = {}) {
824+
//Generate SVG
825+
const margin = {top:10, left:10, right:10, bottom:45}
826+
const d3n = new D3node()
827+
const svg = d3n.createSVG(width, height)
828+
829+
//Data
830+
const X = data.map(({x}) => x)
831+
const start = X.at(0)
832+
const end = X.at(-1)
833+
const Y = data.map(({y}) => y)
834+
const extremum = Math.max(...Y)
835+
high = !Number.isNaN(high) ? high : extremum
836+
low = !Number.isNaN(low) ? low : 0
837+
const T = data.map(({text}, i) => text ?? Y[i])
838+
839+
//Time range
840+
const x = (type === "time" ? d3.scaleTime() : d3.scaleLinear())
841+
.domain([start, end])
842+
.range([margin.top, width - margin.left - margin.right])
843+
let xticks = d3.axisBottom(x).tickSizeOuter(0)
844+
if (labels)
845+
xticks = xticks.tickFormat((_, i) => labels[i])
846+
if (ticks)
847+
xticks = xticks.ticks(ticks)
848+
svg.append("g")
849+
.attr("transform", `translate(${margin.left},${height - margin.bottom})`)
850+
.call(xticks)
851+
.call(g => g.select(".domain").attr("stroke", "rgba(127, 127, 127, .8)"))
852+
.call(g => g.selectAll(".tick line").attr("stroke-opacity", 0.5))
853+
.selectAll("text")
854+
.attr("transform", "translate(-5,5) rotate(-45)")
855+
.style("text-anchor", "end")
856+
.style("font-size", 20)
857+
.attr("fill", "rgba(127, 127, 127, .8)")
858+
859+
//Data range
860+
const y = d3.scaleLinear()
861+
.domain([high, low])
862+
.range([margin.left, height - margin.top - margin.bottom])
863+
svg.append("g")
864+
.attr("transform", `translate(${margin.left},${margin.top})`)
865+
.call(d3.axisRight(y).ticks(Math.round(height/50)).tickSize(width - margin.left - margin.right))
866+
.call(g => g.select(".domain").remove())
867+
.call(g => g.selectAll(".tick line").attr("stroke-opacity", 0.5).attr("stroke-dasharray", "2,2"))
868+
.call(g => g.selectAll(".tick text").attr("x", 0).attr("dy", -4))
869+
.selectAll("text")
870+
.style("font-size", 20)
871+
.attr("fill", "rgba(127, 127, 127, .8)")
872+
873+
//Generate graph line
874+
const datum = Y.map((y, i) => [X.at(i), y])
875+
const tdatum = Y.map((y, i) => [X.at(i), y, T[i]])
876+
const xticked = xticks.scale().ticks(xticks.ticks()[0])
877+
const yticked = match?.(tdatum, xticked) ?? tdatum
878+
svg.append("path")
879+
.datum(datum)
880+
.attr("transform", `translate(${margin.left},${margin.top})`)
881+
.attr(
882+
"d",
883+
d3.line()
884+
.curve(d3.curveLinear)
885+
.x(d => x(d[0]))
886+
.y(d => y(d[1]))
887+
)
888+
.attr("fill", "transparent")
889+
.attr("stroke", "#87ceeb")
890+
.attr("stroke-width", 2)
891+
892+
//Generate graph area
893+
if (area) {
894+
svg.append("path")
895+
.datum(datum)
896+
.attr("transform", `translate(${margin.left},${margin.top})`)
897+
.attr(
898+
"d",
899+
d3.area()
900+
.curve(d3.curveLinear)
901+
.x(d => x(d[0]))
902+
.y0(d => y(d[1]))
903+
.y1(() => y(low)),
904+
)
905+
.attr("fill", "rgba(88, 166, 255, .1)")
906+
}
907+
908+
//Generate graph points
909+
if (points) {
910+
svg.append("g")
911+
.selectAll("circle")
912+
.data(yticked)
913+
.join("circle")
914+
.attr("transform", `translate(${margin.left},${margin.top})`)
915+
.attr("cx", d => x(d[0]))
916+
.attr("cy", d => y(d[1]))
917+
.attr("r", 2)
918+
.attr("fill", "#106cbc")
919+
}
920+
921+
//Generate graph text
922+
if (text) {
923+
svg.append("g")
924+
.attr("fill", "currentColor")
925+
.attr("text-anchor", "middle")
926+
.attr("font-family", "sans-serif")
927+
.attr("font-size", 10)
928+
.attr("stroke", "white")
929+
.attr("stroke-linejoin", "round")
930+
.attr("stroke-width", 4)
931+
.attr("paint-order", "stroke fill")
932+
.selectAll("text")
933+
.data(yticked)
934+
.join("text")
935+
.attr("transform", `translate(${margin.left},${margin.top-4})`)
936+
.attr("x", d => x(d[0]))
937+
.attr("y", d => y(d[1]))
938+
.text(d => d[2] ? d[2] : "")
939+
.attr("fill", "rgba(127, 127, 127, .8)")
940+
}
941+
942+
return d3n.svgString()
943+
},
944+
/**Pie Graph */
945+
pie(data, {colors, width = 480, height = 315} = {}) {
946+
//Generate SVG
947+
const radius = Math.min(width, height) / 2
948+
const d3n = new D3node()
949+
const svg = d3n.createSVG(width, height)
950+
951+
//Data
952+
const K = Object.keys(data)
953+
const V = Object.values(data)
954+
const I = d3.range(K.length).filter(i => !Number.isNaN(V[i]))
955+
956+
//Construct arcs
957+
const color = d3.scaleOrdinal(K, d3.schemeSpectral[K.length])
958+
const arcs = d3.pie().padAngle(1/radius).sort(null).value(i => V[i])(I)
959+
const arc = d3.arc().innerRadius(0).outerRadius(radius)
960+
const labels = d3.arc().innerRadius(radius/2).outerRadius(radius/2)
961+
962+
svg.append("g")
963+
.attr("transform", `translate(${width/2},${height/2})`)
964+
.attr("stroke", "white")
965+
.attr("stroke-width", 1)
966+
.attr("stroke-linejoin", "round")
967+
.selectAll("path")
968+
.data(arcs)
969+
.join("path")
970+
.attr("fill", d => colors?.[K[d.data]] ?? color(K[d.data]))
971+
.attr("d", arc)
972+
.append("title")
973+
.text(d => `${K[d.data]}\n${V[d.data]}`)
974+
975+
svg.append("g")
976+
.attr("transform", `translate(${width/2},${height/2})`)
977+
.attr("font-family", "sans-serif")
978+
.attr("font-size", 12)
979+
.attr("text-anchor", "middle")
980+
.attr("fill", "white")
981+
.attr("stroke", "rbga(0,0,0,.9)")
982+
.attr("paint-order", "stroke fill")
983+
.selectAll("text")
984+
.data(arcs)
985+
.join("text")
986+
.attr("transform", d => `translate(${labels.centroid(d)})`)
987+
.selectAll("tspan")
988+
.data(d => {
989+
const lines = `${K[d.data]}\n${V[d.data]}`.split(/\n/)
990+
return (d.endAngle - d.startAngle) > 0.25 ? lines : lines.slice(0, 1)
991+
})
992+
.join("tspan")
993+
.attr("x", 0)
994+
.attr("y", (_, i) => `${i * 1.1}em`)
995+
.attr("font-weight", (_, i) => i ? null : "bold")
996+
.text(d => d)
997+
998+
return d3n.svgString()
999+
}
1000+
}

0 commit comments

Comments
 (0)