chore(deps): migrate from chartist to d3 (closes #1142)

This commit is contained in:
Simon Lecoq
2023-03-15 19:14:55 -04:00
parent b5ada3d139
commit 6ddca1810a
4 changed files with 190 additions and 12 deletions

View File

@@ -211,7 +211,6 @@ Nixinova
NOASSERTION
nocase
nodeca
nodechartist
nodejs
notoken
octicon

View File

@@ -119,8 +119,6 @@ Below is a list of used packages.
* To parse and handle emojis/[twemojis](https://github.com/twitter/twemoji)
* [jshemas/openGraphScraper](https://github.com/jshemas/openGraphScraper)
* To retrieve open graphs metadata
* [panosoft/node-chartist](https://github.com/panosoft/node-chartist) and [gionkunz/chartist-js](https://github.com/gionkunz/chartist-js)
* To display embed SVG charts
* [rbren/rss-parser](https://github.com/rbren/rss-parser)
* To parse RSS streams
* [Nixinova/Linguist](https://github.com/Nixinova/Linguist)

View File

@@ -84,7 +84,6 @@
"color": "^4.2.3",
"gifencoder": "^2.0.1",
"libxmljs2": "^0.31.0",
"node-chartist": "^1.0.5",
"rss": "^1.2.2",
"yargs-parser": "^21.1.1"
}

View File

@@ -207,14 +207,6 @@ export function stripemojis(string) {
return string.replace(/[^\p{L}\p{N}\p{P}\p{Z}^$\n]/gu, "")
}
/**Chartist */
export async function chartist() {
const css = `<style data-optimizable="true">${await fs.readFile(paths.join(__module(import.meta.url), "../../../node_modules", "node-chartist/dist/main.css")).catch(_ => "")}</style>`
const {default: nodechartist} = await import(url.pathToFileURL(paths.join(__module(import.meta.url), "../../../node_modules", "/node-chartist/lib/index.js")))
return (await nodechartist(...arguments))
.replace(/class="ct-chart-line">/, `class="ct-chart-line">${css}`)
}
/**Language analyzer (single file) */
export async function language({filename, patch}) {
console.debug(`metrics/language > ${filename}`)
@@ -816,3 +808,193 @@ export class D3node {
return this.element.select("svg").node()?.outerHTML || ""
}
}
/** Graph utilities */
export const Graph = {
/**Timeline graph */
timeline() {
return this.graph("time", ...arguments)
},
/**Line graph */
line() {
return this.graph("line", ...arguments)
},
/**Basic Graph */
graph(type, data, {area = true, points = true, text = true, low = NaN, high = NaN, match = null, labels = null, width = 480, height = 315, ticks = 0} = {}) {
//Generate SVG
const margin = {top:10, left:10, right:10, bottom:45}
const d3n = new D3node()
const svg = d3n.createSVG(width, height)
//Data
const X = data.map(({x}) => x)
const start = X.at(0)
const end = X.at(-1)
const Y = data.map(({y}) => y)
const extremum = Math.max(...Y)
high = !Number.isNaN(high) ? high : extremum
low = !Number.isNaN(low) ? low : 0
const T = data.map(({text}, i) => text ?? Y[i])
//Time range
const x = (type === "time" ? d3.scaleTime() : d3.scaleLinear())
.domain([start, end])
.range([margin.top, width - margin.left - margin.right])
let xticks = d3.axisBottom(x).tickSizeOuter(0)
if (labels)
xticks = xticks.tickFormat((_, i) => labels[i])
if (ticks)
xticks = xticks.ticks(ticks)
svg.append("g")
.attr("transform", `translate(${margin.left},${height - margin.bottom})`)
.call(xticks)
.call(g => g.select(".domain").attr("stroke", "rgba(127, 127, 127, .8)"))
.call(g => g.selectAll(".tick line").attr("stroke-opacity", 0.5))
.selectAll("text")
.attr("transform", "translate(-5,5) rotate(-45)")
.style("text-anchor", "end")
.style("font-size", 20)
.attr("fill", "rgba(127, 127, 127, .8)")
//Data range
const y = d3.scaleLinear()
.domain([high, low])
.range([margin.left, height - margin.top - margin.bottom])
svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`)
.call(d3.axisRight(y).ticks(Math.round(height/50)).tickSize(width - margin.left - margin.right))
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").attr("stroke-opacity", 0.5).attr("stroke-dasharray", "2,2"))
.call(g => g.selectAll(".tick text").attr("x", 0).attr("dy", -4))
.selectAll("text")
.style("font-size", 20)
.attr("fill", "rgba(127, 127, 127, .8)")
//Generate graph line
const datum = Y.map((y, i) => [X.at(i), y])
const tdatum = Y.map((y, i) => [X.at(i), y, T[i]])
const xticked = xticks.scale().ticks(xticks.ticks()[0])
const yticked = match?.(tdatum, xticked) ?? tdatum
svg.append("path")
.datum(datum)
.attr("transform", `translate(${margin.left},${margin.top})`)
.attr(
"d",
d3.line()
.curve(d3.curveLinear)
.x(d => x(d[0]))
.y(d => y(d[1]))
)
.attr("fill", "transparent")
.attr("stroke", "#87ceeb")
.attr("stroke-width", 2)
//Generate graph area
if (area) {
svg.append("path")
.datum(datum)
.attr("transform", `translate(${margin.left},${margin.top})`)
.attr(
"d",
d3.area()
.curve(d3.curveLinear)
.x(d => x(d[0]))
.y0(d => y(d[1]))
.y1(() => y(low)),
)
.attr("fill", "rgba(88, 166, 255, .1)")
}
//Generate graph points
if (points) {
svg.append("g")
.selectAll("circle")
.data(yticked)
.join("circle")
.attr("transform", `translate(${margin.left},${margin.top})`)
.attr("cx", d => x(d[0]))
.attr("cy", d => y(d[1]))
.attr("r", 2)
.attr("fill", "#106cbc")
}
//Generate graph text
if (text) {
svg.append("g")
.attr("fill", "currentColor")
.attr("text-anchor", "middle")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("stroke", "white")
.attr("stroke-linejoin", "round")
.attr("stroke-width", 4)
.attr("paint-order", "stroke fill")
.selectAll("text")
.data(yticked)
.join("text")
.attr("transform", `translate(${margin.left},${margin.top-4})`)
.attr("x", d => x(d[0]))
.attr("y", d => y(d[1]))
.text(d => d[2] ? d[2] : "")
.attr("fill", "rgba(127, 127, 127, .8)")
}
return d3n.svgString()
},
/**Pie Graph */
pie(data, {colors, width = 480, height = 315} = {}) {
//Generate SVG
const radius = Math.min(width, height) / 2
const d3n = new D3node()
const svg = d3n.createSVG(width, height)
//Data
const K = Object.keys(data)
const V = Object.values(data)
const I = d3.range(K.length).filter(i => !Number.isNaN(V[i]))
//Construct arcs
const color = d3.scaleOrdinal(K, d3.schemeSpectral[K.length])
const arcs = d3.pie().padAngle(1/radius).sort(null).value(i => V[i])(I)
const arc = d3.arc().innerRadius(0).outerRadius(radius)
const labels = d3.arc().innerRadius(radius/2).outerRadius(radius/2)
svg.append("g")
.attr("transform", `translate(${width/2},${height/2})`)
.attr("stroke", "white")
.attr("stroke-width", 1)
.attr("stroke-linejoin", "round")
.selectAll("path")
.data(arcs)
.join("path")
.attr("fill", d => colors?.[K[d.data]] ?? color(K[d.data]))
.attr("d", arc)
.append("title")
.text(d => `${K[d.data]}\n${V[d.data]}`)
svg.append("g")
.attr("transform", `translate(${width/2},${height/2})`)
.attr("font-family", "sans-serif")
.attr("font-size", 12)
.attr("text-anchor", "middle")
.attr("fill", "white")
.attr("stroke", "rbga(0,0,0,.9)")
.attr("paint-order", "stroke fill")
.selectAll("text")
.data(arcs)
.join("text")
.attr("transform", d => `translate(${labels.centroid(d)})`)
.selectAll("tspan")
.data(d => {
const lines = `${K[d.data]}\n${V[d.data]}`.split(/\n/)
return (d.endAngle - d.startAngle) > 0.25 ? lines : lines.slice(0, 1)
})
.join("tspan")
.attr("x", 0)
.attr("y", (_, i) => `${i * 1.1}em`)
.attr("font-weight", (_, i) => i ? null : "bold")
.text(d => d)
return d3n.svgString()
}
}