From 17c98a84acf4a04bd66317280a1d9056468c966b Mon Sep 17 00:00:00 2001 From: GussevPM Date: Thu, 14 Aug 2025 22:40:53 +0200 Subject: [PATCH 01/13] Fix badge loading --- components/modules/rollup/RollupOverview.vue | 10 +++++--- nuxt.config.ts | 1 + package.json | 5 ++-- scripts/generateBadgesList.js | 27 ++++++++++++++++++++ server/api/badges.get.js | 2 +- services/config.js | 4 +-- src/data/badges.json | 16 ++++++++++++ 7 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 scripts/generateBadgesList.js create mode 100644 src/data/badges.json diff --git a/components/modules/rollup/RollupOverview.vue b/components/modules/rollup/RollupOverview.vue index 60454db3..5e0d1d5a 100644 --- a/components/modules/rollup/RollupOverview.vue +++ b/components/modules/rollup/RollupOverview.vue @@ -22,6 +22,9 @@ import { getRankCategory } from "@/services/constants/rollups" /** API */ import { fetchRollupBlobs, fetchRollupExportData, fetchRollupNamespaces } from "@/services/api/rollup" +/** Data */ +import badges from "@data/badges.json" + /** Store */ import { useCacheStore } from "@/store/cache.store" import { useNotificationsStore } from "@/store/notifications.store" @@ -100,10 +103,9 @@ const tags = computed(() => }, []), ) -const { data: badges } = await useFetch('/api/badges') const showBadges = computed(() => { - const showSettled = props.rollup?.settled_on && badges.value?.settled?.includes(props.rollup?.settled_on?.toLowerCase()) - const showProvider = props.rollup?.provider && badges.value?.providers?.includes(props.rollup?.provider?.toLowerCase()) + const showSettled = props.rollup?.settled_on && badges?.settled?.includes(props.rollup?.settled_on?.toLowerCase()) + const showProvider = props.rollup?.provider && badges?.provider?.includes(props.rollup?.provider?.toLowerCase()) return { show: showSettled || showProvider, @@ -416,7 +418,7 @@ const handleCSVDownload = async (value) => { /> - + diff --git a/nuxt.config.ts b/nuxt.config.ts index 75d265bd..2be8311f 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -143,6 +143,7 @@ export default defineNuxtConfig({ resolve: { alias: { "unenv/runtime/node/buffer/index/": path.resolve(__dirname, "./node_modules/buffer/index"), + "@data": path.resolve(__dirname, "src/data"), }, }, plugins: [wasm(), topLevelAwait(), nodePolyfills()], diff --git a/package.json b/package.json index 22de3353..a5785fe6 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,10 @@ "license": "MIT", "type": "module", "scripts": { - "build": "nuxt build", - "dev": "cross-env PORT=9090 nuxt dev", + "build": "npm run generate-badges && nuxt build", + "dev": "npm run generate-badges && cross-env PORT=9090 nuxt dev", "generate": "nuxt generate", + "generate-badges": "node scripts/generateBadgesList.js", "preview": "nuxt preview", "postinstall": "nuxt prepare", "lint": "oxlint" diff --git a/scripts/generateBadgesList.js b/scripts/generateBadgesList.js new file mode 100644 index 00000000..77a484ef --- /dev/null +++ b/scripts/generateBadgesList.js @@ -0,0 +1,27 @@ +import fs from "fs" +import path from "path" + +const folders = ["provider", "settled"] +const publicPath = path.resolve("public/img/badges") + +let result = {} + +for (const folder of folders) { + const folderPath = path.join(publicPath, folder) + const files = fs.readdirSync(folderPath) + .filter(file => !file.startsWith(".")) + .map(file => path.parse(file).name) + + result[folder] = files +} + +const outputDir = path.resolve("src/data"); +fs.mkdirSync(outputDir, { recursive: true }); + +const outputPath = path.join(outputDir, "badges.json"); +fs.writeFileSync( + outputPath, + JSON.stringify(result, null, 2) +) + +console.log("✅ Badges list generated:", result); diff --git a/server/api/badges.get.js b/server/api/badges.get.js index bb0c9a32..6b27faff 100644 --- a/server/api/badges.get.js +++ b/server/api/badges.get.js @@ -12,7 +12,7 @@ export default defineEventHandler(() => { } return { - providers: getFiles('provider'), + provider: getFiles('provider'), settled: getFiles('settled') } }) diff --git a/services/config.js b/services/config.js index d67df3e3..2c78e06e 100644 --- a/services/config.js +++ b/services/config.js @@ -42,7 +42,7 @@ export const useServerURL = () => { return Server.API.dev default: - return Server.API.mainnet + return Server.API.mocha } } @@ -69,7 +69,7 @@ export const useSocketURL = () => { return Server.WSS.dev default: - return Server.WSS.mainnet + return Server.WSS.mocha } } diff --git a/src/data/badges.json b/src/data/badges.json new file mode 100644 index 00000000..8c9331d7 --- /dev/null +++ b/src/data/badges.json @@ -0,0 +1,16 @@ +{ + "provider": [ + "bvm.network", + "caldera", + "conduit", + "dymension", + "gelato", + "initia", + "movement" + ], + "settled": [ + "arbitrum", + "base", + "ethereum" + ] +} \ No newline at end of file From 94b74e0c3cc2d5569fd4a7ed38cbf3deb8e3b18b Mon Sep 17 00:00:00 2001 From: sstark21 Date: Fri, 15 Aug 2025 12:39:17 +0100 Subject: [PATCH 02/13] refactor: optimize line charts --- components/modules/address/AddressCharts.vue | 175 +------------- .../modules/namespace/NamespaceCharts.vue | 173 +------------- components/modules/rollup/RollupCharts.vue | 190 +-------------- services/utils/charts.js | 220 ++++++++++++++++++ services/utils/index.js | 1 + 5 files changed, 227 insertions(+), 532 deletions(-) create mode 100644 services/utils/charts.js diff --git a/components/modules/address/AddressCharts.vue b/components/modules/address/AddressCharts.vue index 233053bb..e487018c 100644 --- a/components/modules/address/AddressCharts.vue +++ b/components/modules/address/AddressCharts.vue @@ -12,6 +12,7 @@ import Toggle from "@/components/ui/Toggle.vue" /** Services */ import { abbreviate, tia } from "@/services/utils" +import { buildLineChart } from '@/services/utils/charts' /** API */ import { fetchAddressSeries } from "@/services/api/stats" @@ -103,180 +104,6 @@ const badgeEl = ref() const badgeText = ref("") const badgeOffset = ref(0) -const buildLineChart = (chartEl, data, onEnter, onLeave, metric) => { - const width = chartWrapperEl.value.wrapper.getBoundingClientRect().width - const height = 180 - const marginTop = 0 - const marginRight = 0 - const marginBottom = 24 - const marginLeft = 52 - - const MAX_VALUE = d3.max(data, (d) => d.value) ? d3.max(data, (d) => d.value) : 1 - - /** Scale */ - const x = d3.scaleUtc( - d3.extent(data, (d) => d.date), - [marginLeft, width - marginRight], - ) - const y = d3.scaleLinear([0, MAX_VALUE], [height - marginBottom - 6, marginTop]) - const line = d3 - .line() - .x((d) => x(d.date)) - .y((d) => y(d.value)) - - /** Tooltip */ - const bisect = d3.bisector((d) => d.date).center - const onPointermoved = (event) => { - onEnter() - - const idx = bisect(data, x.invert(d3.pointer(event)[0])) - - tooltipXOffset.value = x(data[idx].date) - tooltipYDataOffset.value = y(data[idx].value) - tooltipYOffset.value = event.layerY - tooltipText.value = data[idx].value - - if (tooltipEl.value) { - if (idx > parseInt(selectedPeriod.value.value / 2)) { - tooltipDynamicXPosition.value = tooltipXOffset.value - tooltipEl.value.wrapper.getBoundingClientRect().width - 16 - } else { - tooltipDynamicXPosition.value = tooltipXOffset.value + 16 - } - } - - let tf = selectedPeriod.value.timeframe - if (metric === "tvl" && ["hour", "week"].includes(selectedPeriod.value.timeframe)) { - tf = "day" - } - badgeText.value = - tf === "month" - ? DateTime.fromJSDate(data[idx].date).toFormat("LLL") - : tf === "day" - ? DateTime.fromJSDate(data[idx].date).toFormat("LLL dd") - : DateTime.fromJSDate(data[idx].date).set({ minutes: 0 }).toFormat("hh:mm a") - - if (!badgeEl.value) return - const badgeWidth = badgeEl.value.getBoundingClientRect().width - if (tooltipXOffset.value - marginLeft < badgeWidth / 2) { - badgeOffset.value = 0 - } else if (badgeWidth + tooltipXOffset.value > width) { - badgeOffset.value = Math.abs(width - (badgeWidth + tooltipXOffset.value)) + (data.length - 1 - idx) * 2 - } else { - badgeOffset.value = badgeWidth / 2 - } - } - const onPointerleft = () => { - onLeave() - badgeText.value = "" - } - - /** SVG Container */ - const svg = d3 - .create("svg") - .attr("width", width) - .attr("height", height) - .attr("viewBox", [0, 0, width, height]) - .attr("preserveAspectRatio", "none") - .attr("style", "max-width: 100%; height: intrinsic;") - .style("-webkit-tap-highlight-color", "transparent") - .on("pointerenter pointermove", onPointermoved) - .on("pointerleave", onPointerleft) - .on("touchstart", (event) => event.preventDefault()) - - /** Vertical Lines */ - svg.append("path") - .attr("fill", "none") - .attr("stroke", "var(--op-10)") - .attr("stroke-width", 2) - .attr("d", `M${marginLeft},${height - marginBottom + 2} L${marginLeft},${height - marginBottom - 5}`) - svg.append("path") - .attr("fill", "none") - .attr("stroke", "var(--op-10)") - .attr("stroke-width", 2) - .attr("d", `M${width - 1},${height - marginBottom + 2} L${width - 1},${height - marginBottom - 5}`) - - /** Default Horizontal Line */ - svg.append("path") - .attr("fill", "none") - .attr("stroke", "var(--op-10)") - .attr("stroke-width", 2) - .attr("d", `M${0},${height - marginBottom - 6} L${width},${height - marginBottom - 6}`) - - /** Chart Line */ - let path1 = null - let path2 = null - path1 = svg - .append("path") - .attr("fill", "none") - .attr("stroke", "var(--brand)") - .attr("stroke-width", 2) - .attr("stroke-linecap", "round") - .attr("stroke-linejoin", "round") - .attr("d", line(loadLastValue.value ? data.slice(0, data.length - 1) : data)) - - if (loadLastValue.value) { - // Create pattern - const defs = svg.append("defs") - const pattern = defs - .append("pattern") - .attr("id", "dashedPattern") - .attr("width", 8) - .attr("height", 2) - .attr("patternUnits", "userSpaceOnUse") - pattern.append("rect").attr("width", 4).attr("height", 2).attr("fill", "var(--brand)") - pattern.append("rect").attr("x", 8).attr("width", 4).attr("height", 2).attr("fill", "transparent") - - // Last dash segment - path2 = svg - .append("path") - .attr("fill", "none") - .attr("stroke", "url(#dashedPattern)") - .attr("stroke-width", 2) - .attr("stroke-linecap", "round") - .attr("stroke-linejoin", "round") - .attr("d", line(data.slice(data.length - 2, data.length))) - } - - const totalDuration = 1_000 - const path1Duration = loadLastValue.value ? (totalDuration / data.length) * (data.length - 1) : totalDuration - const path1Length = path1.node().getTotalLength() - - path1 - .attr("stroke-dasharray", path1Length) - .attr("stroke-dashoffset", path1Length) - .transition() - .duration(path1Duration) - .ease(d3.easeLinear) - .attr("stroke-dashoffset", 0) - - if (loadLastValue.value) { - const path2Duration = totalDuration / data.length - const path2Length = path2.node().getTotalLength() + 1 - - path2 - .attr("stroke-dasharray", path2Length) - .attr("stroke-dashoffset", path2Length) - .transition() - .duration(path2Duration) - .ease(d3.easeLinear) - .delay(path1Duration) - .attr("stroke-dashoffset", 0) - } - - const point = svg - .append("circle") - .attr("cx", x(data[data.length - 1].date)) - .attr("cy", y(data[data.length - 1].value)) - .attr("fill", "var(--brand)") - .attr("r", 3) - .attr("opacity", 0) - - point.transition().delay(totalDuration).duration(200).attr("opacity", 1) - - if (chartEl.children[0]) chartEl.children[0].remove() - chartEl.append(svg.node()) -} - const buildBarChart = (chartEl, data, onEnter, onLeave, metric) => { const width = chartWrapperEl.value.wrapper.getBoundingClientRect().width const height = 180 diff --git a/components/modules/namespace/NamespaceCharts.vue b/components/modules/namespace/NamespaceCharts.vue index 56d838bd..4f2af529 100644 --- a/components/modules/namespace/NamespaceCharts.vue +++ b/components/modules/namespace/NamespaceCharts.vue @@ -12,6 +12,7 @@ import Toggle from "@/components/ui/Toggle.vue" /** Services */ import { abbreviate, formatBytes } from "@/services/utils" +import { buildLineChart } from "~/services/utils/charts" /** API */ import { fetchNamespaceSeries } from "@/services/api/stats" @@ -134,176 +135,6 @@ const xAxisLabels = computed(() => { return labels }) -const buildLineChart = (chartEl, data, onEnter, onLeave) => { - const width = chartWrapperEl.value.wrapper.getBoundingClientRect().width - const height = 180 - const marginTop = 0 - const marginRight = 0 - const marginBottom = 24 - const marginLeft = 52 - - const MAX_VALUE = d3.max(data, (d) => d.value) ? d3.max(data, (d) => d.value) : 1 - - /** Scale */ - const x = d3.scaleUtc( - d3.extent(data, (d) => d.date), - [marginLeft, width - marginRight], - ) - const y = d3.scaleLinear([0, MAX_VALUE], [height - marginBottom - 6, marginTop]) - const line = d3 - .line() - .x((d) => x(d.date)) - .y((d) => y(d.value)) - - /** Tooltip */ - const bisect = d3.bisector((d) => d.date).center - const onPointermoved = (event) => { - onEnter() - - const idx = bisect(data, x.invert(d3.pointer(event)[0])) - - tooltipXOffset.value = x(data[idx].date) - tooltipYDataOffset.value = y(data[idx].value) - tooltipYOffset.value = event.layerY - tooltipText.value = data[idx].value - - if (tooltipEl.value) { - if (idx > parseInt(selectedPeriod.value.value / 2)) { - tooltipDynamicXPosition.value = tooltipXOffset.value - tooltipEl.value.wrapper.getBoundingClientRect().width - 16 - } else { - tooltipDynamicXPosition.value = tooltipXOffset.value + 16 - } - } - - badgeText.value = - selectedPeriod.value.timeframe === "month" - ? DateTime.fromJSDate(data[idx].date).toFormat("LLL") - : selectedPeriod.value.timeframe === "day" - ? DateTime.fromJSDate(data[idx].date).toFormat("LLL dd") - : DateTime.fromJSDate(data[idx].date).set({ minutes: 0 }).toFormat("hh:mm a") - - if (!badgeEl.value) return - const badgeWidth = badgeEl.value.getBoundingClientRect().width - if (tooltipXOffset.value - marginLeft < badgeWidth / 2) { - badgeOffset.value = 0 - } else if (badgeWidth + tooltipXOffset.value > width) { - badgeOffset.value = Math.abs(width - (badgeWidth + tooltipXOffset.value)) + (data.length - 1 - idx) * 2 - } else { - badgeOffset.value = badgeWidth / 2 - } - } - const onPointerleft = () => { - onLeave() - badgeText.value = "" - } - - /** SVG Container */ - const svg = d3 - .create("svg") - .attr("width", width) - .attr("height", height) - .attr("viewBox", [0, 0, width, height]) - .attr("preserveAspectRatio", "none") - .attr("style", "max-width: 100%; height: intrinsic;") - .style("-webkit-tap-highlight-color", "transparent") - .on("pointerenter pointermove", onPointermoved) - .on("pointerleave", onPointerleft) - .on("touchstart", (event) => event.preventDefault()) - - /** Vertical Lines */ - svg.append("path") - .attr("fill", "none") - .attr("stroke", "var(--op-10)") - .attr("stroke-width", 2) - .attr("d", `M${marginLeft},${height - marginBottom + 2} L${marginLeft},${height - marginBottom - 5}`) - svg.append("path") - .attr("fill", "none") - .attr("stroke", "var(--op-10)") - .attr("stroke-width", 2) - .attr("d", `M${width - 1},${height - marginBottom + 2} L${width - 1},${height - marginBottom - 5}`) - - /** Default Horizontal Line */ - svg.append("path") - .attr("fill", "none") - .attr("stroke", "var(--op-10)") - .attr("stroke-width", 2) - .attr("d", `M${0},${height - marginBottom - 6} L${width},${height - marginBottom - 6}`) - - /** Chart Line */ - let path1 = null - let path2 = null - path1 = svg - .append("path") - .attr("fill", "none") - .attr("stroke", "var(--brand)") - .attr("stroke-width", 2) - .attr("stroke-linecap", "round") - .attr("stroke-linejoin", "round") - .attr("d", line(loadLastValue.value ? data.slice(0, data.length - 1) : data)) - - if (loadLastValue.value) { - // Create pattern - const defs = svg.append("defs") - const pattern = defs - .append("pattern") - .attr("id", "dashedPattern") - .attr("width", 8) - .attr("height", 2) - .attr("patternUnits", "userSpaceOnUse") - pattern.append("rect").attr("width", 4).attr("height", 2).attr("fill", "var(--brand)") - pattern.append("rect").attr("x", 8).attr("width", 4).attr("height", 2).attr("fill", "transparent") - - // Last dash segment - path2 = svg - .append("path") - .attr("fill", "none") - .attr("stroke", "url(#dashedPattern)") - .attr("stroke-width", 2) - .attr("stroke-linecap", "round") - .attr("stroke-linejoin", "round") - .attr("d", line(data.slice(data.length - 2, data.length))) - } - - const totalDuration = 1_000 - const path1Duration = loadLastValue.value ? (totalDuration / data.length) * (data.length - 1) : totalDuration - const path1Length = path1.node().getTotalLength() - - path1 - .attr("stroke-dasharray", path1Length) - .attr("stroke-dashoffset", path1Length) - .transition() - .duration(path1Duration) - .ease(d3.easeLinear) - .attr("stroke-dashoffset", 0) - - if (loadLastValue.value) { - const path2Duration = totalDuration / data.length - const path2Length = path2.node().getTotalLength() + 1 - - path2 - .attr("stroke-dasharray", path2Length) - .attr("stroke-dashoffset", path2Length) - .transition() - .duration(path2Duration) - .ease(d3.easeLinear) - .delay(path1Duration) - .attr("stroke-dashoffset", 0) - } - - const point = svg - .append("circle") - .attr("cx", x(data[data.length - 1].date)) - .attr("cy", y(data[data.length - 1].value)) - .attr("fill", "var(--brand)") - .attr("r", 3) - .attr("opacity", 0) - - point.transition().delay(totalDuration).duration(200).attr("opacity", 1) - - if (chartEl.children[0]) chartEl.children[0].remove() - chartEl.append(svg.node()) -} - const buildBarChart = (chartEl, data, onEnter, onLeave, metric) => { const width = chartWrapperEl.value.wrapper.getBoundingClientRect().width const height = 180 @@ -548,12 +379,14 @@ const buildNamespaceCharts = async (loadData = true) => { loadLastValue.value ? sizeSeries.value : sizeSeries.value.slice(0, sizeSeries.value.length - 1), () => (showSeriesTooltip.value = true), () => (showSeriesTooltip.value = false), + "size", ) buildLineChart( pfbSeriesChartEl.value.wrapper, loadLastValue.value ? pfbSeries.value : pfbSeries.value.slice(0, pfbSeries.value.length - 1), () => (showPfbTooltip.value = true), () => (showPfbTooltip.value = false), + "pfb", ) } else { buildBarChart( diff --git a/components/modules/rollup/RollupCharts.vue b/components/modules/rollup/RollupCharts.vue index a131ea59..36597dad 100644 --- a/components/modules/rollup/RollupCharts.vue +++ b/components/modules/rollup/RollupCharts.vue @@ -14,6 +14,7 @@ import Tooltip from "@/components/ui/Tooltip.vue" /** Services */ import { abbreviate, formatBytes, sortArrayOfObjects, spaces, tia } from "@/services/utils" +import { buildLineChart } from "@/services/utils/charts" /** API */ import { fetchRollupSeries } from "@/services/api/stats" @@ -157,194 +158,6 @@ const getXAxisLabels = (start, tvl = false) => { return res } -const buildLineChart = (chartEl, data, onEnter, onLeave, metric) => { - const width = chartWrapperEl.value.wrapper.getBoundingClientRect().width - const height = 180 - const marginTop = 0 - const marginRight = 0 - const marginBottom = 24 - const marginLeft = 52 - - const MAX_VALUE = d3.max(data, (d) => d.value) ? d3.max(data, (d) => d.value) : 1 - - /** Scale */ - const x = d3.scaleUtc( - d3.extent(data, (d) => d.date), - [marginLeft, width - marginRight], - ) - const y = d3.scaleLinear([0, MAX_VALUE], [height - marginBottom - 6, marginTop]) - const line = d3 - .line() - .x((d) => x(d.date)) - .y((d) => y(d.value)) - - /** Tooltip */ - const bisect = d3.bisector((d) => d.date).center - const onPointermoved = (event) => { - if (!data.length) return - - onEnter() - - const idx = bisect(data, x.invert(d3.pointer(event)[0])) - - tooltipXOffset.value = x(data[idx].date) - tooltipYDataOffset.value = y(data[idx].value) - tooltipYOffset.value = event.layerY - tooltipText.value = data[idx].value - - if (tooltipEl.value) { - if (idx > parseInt(selectedPeriod.value.value / 2)) { - tooltipDynamicXPosition.value = tooltipXOffset.value - tooltipEl.value.wrapper.getBoundingClientRect().width - 16 - } else { - tooltipDynamicXPosition.value = tooltipXOffset.value + 16 - } - } - - let tf = selectedPeriod.value.timeframe - if (metric === "tvl" && ["hour", "week"].includes(selectedPeriod.value.timeframe)) { - tf = "day" - } - badgeText.value = - tf === "month" - ? DateTime.fromJSDate(data[idx].date).toFormat("LLL") - : tf === "day" - ? DateTime.fromJSDate(data[idx].date).toFormat("LLL dd") - : DateTime.fromJSDate(data[idx].date).set({ minutes: 0 }).toFormat("hh:mm a") - - if (!badgeEl.value) return - const badgeWidth = badgeEl.value.getBoundingClientRect().width - if (tooltipXOffset.value - marginLeft < badgeWidth / 2) { - badgeOffset.value = 0 - } else if (badgeWidth + tooltipXOffset.value > width) { - badgeOffset.value = Math.abs(width - (badgeWidth + tooltipXOffset.value)) + (data.length - 1 - idx) * 2 - } else { - badgeOffset.value = badgeWidth / 2 - } - } - const onPointerleft = () => { - if (!data.length) return - - onLeave() - badgeText.value = "" - } - - /** SVG Container */ - const svg = d3 - .create("svg") - .attr("width", width) - .attr("height", height) - .attr("viewBox", [0, 0, width, height]) - .attr("preserveAspectRatio", "none") - .attr("style", "max-width: 100%; height: intrinsic;") - .style("-webkit-tap-highlight-color", "transparent") - .on("pointerenter pointermove", onPointermoved) - .on("pointerleave", onPointerleft) - .on("touchstart", (event) => event.preventDefault()) - - /** Vertical Lines */ - svg.append("path") - .attr("fill", "none") - .attr("stroke", "var(--op-10)") - .attr("stroke-width", 2) - .attr("d", `M${marginLeft},${height - marginBottom + 2} L${marginLeft},${height - marginBottom - 5}`) - svg.append("path") - .attr("fill", "none") - .attr("stroke", "var(--op-10)") - .attr("stroke-width", 2) - .attr("d", `M${width - 1},${height - marginBottom + 2} L${width - 1},${height - marginBottom - 5}`) - - /** Default Horizontal Line */ - svg.append("path") - .attr("fill", "none") - .attr("stroke", "var(--op-10)") - .attr("stroke-width", 2) - .attr("d", `M${0},${height - marginBottom - 6} L${width},${height - marginBottom - 6}`) - - if (data.length) { - /** Chart Line */ - let path1 = null - let path2 = null - path1 = svg - .append("path") - .attr("fill", "none") - .attr("stroke", "var(--brand)") - .attr("stroke-width", 2) - .attr("stroke-linecap", "round") - .attr("stroke-linejoin", "round") - .attr("d", line(loadLastValue.value ? data.slice(0, data.length - 1) : data)) - - if (loadLastValue.value) { - // Create pattern - const defs = svg.append("defs") - const pattern = defs - .append("pattern") - .attr("id", "dashedPattern") - .attr("width", 8) - .attr("height", 2) - .attr("patternUnits", "userSpaceOnUse") - pattern.append("rect").attr("width", 4).attr("height", 2).attr("fill", "var(--brand)") - pattern.append("rect").attr("x", 8).attr("width", 4).attr("height", 2).attr("fill", "transparent") - - // Last dash segment - path2 = svg - .append("path") - .attr("fill", "none") - .attr("stroke", "url(#dashedPattern)") - .attr("stroke-width", 2) - .attr("stroke-linecap", "round") - .attr("stroke-linejoin", "round") - .attr("d", line(data.slice(data.length - 2, data.length))) - } - - const totalDuration = 1_000 - const path1Duration = loadLastValue.value ? (totalDuration / data.length) * (data.length - 1) : totalDuration - const path1Length = path1.node().getTotalLength() - - path1 - .attr("stroke-dasharray", path1Length) - .attr("stroke-dashoffset", path1Length) - .transition() - .duration(path1Duration) - .ease(d3.easeLinear) - .attr("stroke-dashoffset", 0) - - if (loadLastValue.value) { - const path2Duration = totalDuration / data.length - const path2Length = path2.node().getTotalLength() + 1 - - path2 - .attr("stroke-dasharray", path2Length) - .attr("stroke-dashoffset", path2Length) - .transition() - .duration(path2Duration) - .ease(d3.easeLinear) - .delay(path1Duration) - .attr("stroke-dashoffset", 0) - } - - const point = svg - .append("circle") - .attr("cx", x(data[data.length - 1].date)) - .attr("cy", y(data[data.length - 1].value)) - .attr("fill", "var(--brand)") - .attr("r", 3) - .attr("opacity", 0) - - point.transition().delay(totalDuration).duration(200).attr("opacity", 1) - } else { - svg.append("text") - .attr("x", width / 2) - .attr("y", height * 0.3) - .attr("text-anchor", "middle") - .attr("fill", "var(--op-20)") - .style("font-size", "14px") - .text("No data available for this rollup") - } - - if (chartEl.children[0]) chartEl.children[0].remove() - chartEl.append(svg.node()) -} - const buildBarChart = (chartEl, data, onEnter, onLeave, metric) => { const width = chartWrapperEl.value.wrapper.getBoundingClientRect().width const height = 180 @@ -773,6 +586,7 @@ const buildRollupCharts = async (loadData = true) => { loadLastValue.value ? sizeSeries.value : sizeSeries.value.slice(0, sizeSeries.value.length - 1), () => (showSeriesTooltip.value = true), () => (showSeriesTooltip.value = false), + "size", ) buildLineChart( pfbSeriesChartEl.value.wrapper, diff --git a/services/utils/charts.js b/services/utils/charts.js new file mode 100644 index 00000000..3835e5e4 --- /dev/null +++ b/services/utils/charts.js @@ -0,0 +1,220 @@ +import * as d3 from "d3" +import { DateTime } from "luxon" + +/** + * Builds a line chart using D3.js + * @param {HTMLElement} chartEl - DOM element for chart placement + * @param {Array} data - Data for the chart [{date, value}] + * @param {Function} onEnter - Callback on hover + * @param {Function} onLeave - Callback on cursor leave + * @param {string} metric - Metric (optional, for special TVL logic) + */ +export const buildLineChart = (chartEl, data, onEnter, onLeave, metric) => { + const width = chartEl.parentElement.getBoundingClientRect().width + const height = 180 + const marginTop = 0 + const marginRight = 0 + const marginBottom = 24 + const marginLeft = 52 + + const MAX_VALUE = d3.max(data, (d) => d.value) ? d3.max(data, (d) => d.value) : 1 + + /** Scale */ + const x = d3.scaleUtc( + d3.extent(data, (d) => d.date), + [marginLeft, width - marginRight], + ) + const y = d3.scaleLinear([0, MAX_VALUE], [height - marginBottom - 6, marginTop]) + const line = d3 + .line() + .x((d) => x(d.date)) + .y((d) => y(d.value)) + + /** Tooltip */ + const bisect = d3.bisector((d) => d.date).center + const onPointermoved = (event) => { + if (!data.length) return + + onEnter() + + const idx = bisect(data, x.invert(d3.pointer(event)[0])) + + const tooltipXOffset = window.currentTooltipXOffset + const tooltipYDataOffset = window.currentTooltipYDataOffset + const tooltipYOffset = window.currentTooltipYOffset + const tooltipText = window.currentTooltipText + const tooltipDynamicXPosition = window.currentTooltipDynamicXPosition + const badgeText = window.currentBadgeText + const badgeOffset = window.currentBadgeOffset + const tooltipEl = window.currentTooltipEl + const badgeEl = window.currentBadgeEl + const selectedPeriod = window.currentSelectedPeriod + const loadLastValue = window.currentLoadLastValue + + if (tooltipXOffset) tooltipXOffset.value = x(data[idx].date) + if (tooltipYDataOffset) tooltipYDataOffset.value = y(data[idx].value) + if (tooltipYOffset) tooltipYOffset.value = event.layerY + if (tooltipText) tooltipText.value = data[idx].value + + if (tooltipEl && tooltipEl.value) { + if (idx > parseInt(selectedPeriod?.value?.value / 2)) { + tooltipDynamicXPosition.value = tooltipXOffset.value - tooltipEl.value.wrapper.getBoundingClientRect().width - 16 + } else { + tooltipDynamicXPosition.value = tooltipXOffset.value + 16 + } + } + + let tf = selectedPeriod?.value?.timeframe + if (metric === "tvl" && ["hour", "week"].includes(selectedPeriod?.value?.timeframe)) { + tf = "day" + } + + if (badgeText) { + badgeText.value = + tf === "month" + ? DateTime.fromJSDate(data[idx].date).toFormat("LLL") + : tf === "day" + ? DateTime.fromJSDate(data[idx].date).toFormat("LLL dd") + : DateTime.fromJSDate(data[idx].date).set({ minutes: 0 }).toFormat("hh:mm a") + } + + if (badgeEl && badgeEl.value) { + const badgeWidth = badgeEl.value.getBoundingClientRect().width + if (tooltipXOffset.value - marginLeft < badgeWidth / 2) { + badgeOffset.value = 0 + } else if (badgeWidth + tooltipXOffset.value > width) { + badgeOffset.value = Math.abs(width - (badgeWidth + tooltipXOffset.value)) + (data.length - 1 - idx) * 2 + } else { + badgeOffset.value = badgeWidth / 2 + } + } + } + + const onPointerleft = () => { + if (!data.length) return + + onLeave() + + const badgeText = window.currentBadgeText + if (badgeText) badgeText.value = "" + } + + /** SVG Container */ + const svg = d3 + .create("svg") + .attr("width", width) + .attr("height", height) + .attr("viewBox", [0, 0, width, height]) + .attr("preserveAspectRatio", "none") + .attr("style", "max-width: 100%; height: intrinsic;") + .style("-webkit-tap-highlight-color", "transparent") + .on("pointerenter pointermove", onPointermoved) + .on("pointerleave", onPointerleft) + .on("touchstart", (event) => event.preventDefault()) + + /** Vertical Lines */ + svg.append("path") + .attr("fill", "none") + .attr("stroke", "var(--op-10)") + .attr("stroke-width", 2) + .attr("d", `M${marginLeft},${height - marginBottom + 2} L${marginLeft},${height - marginBottom - 5}`) + svg.append("path") + .attr("fill", "none") + .attr("stroke", "var(--op-10)") + .attr("stroke-width", 2) + .attr("d", `M${width - 1},${height - marginBottom + 2} L${width - 1},${height - marginBottom - 5}`) + + /** Default Horizontal Line */ + svg.append("path") + .attr("fill", "none") + .attr("stroke", "var(--op-10)") + .attr("stroke-width", 2) + .attr("d", `M${0},${height - marginBottom - 6} L${width},${height - marginBottom - 6}`) + + if (data.length) { + /** Chart Line */ + let path1 = null + let path2 = null + + const loadLastValue = window.currentLoadLastValue + + path1 = svg + .append("path") + .attr("fill", "none") + .attr("stroke", "var(--brand)") + .attr("stroke-width", 2) + .attr("stroke-linecap", "round") + .attr("stroke-linejoin", "round") + .attr("d", line(loadLastValue?.value ? data.slice(0, data.length - 1) : data)) + + if (loadLastValue?.value) { + // Create pattern + const defs = svg.append("defs") + const pattern = defs + .append("pattern") + .attr("id", "dashedPattern") + .attr("width", 8) + .attr("height", 2) + .attr("patternUnits", "userSpaceOnUse") + pattern.append("rect").attr("width", 4).attr("height", 2).attr("fill", "var(--brand)") + pattern.append("rect").attr("x", 8).attr("width", 4).attr("height", 2).attr("fill", "transparent") + + // Last dash segment + path2 = svg + .append("path") + .attr("fill", "none") + .attr("stroke", "url(#dashedPattern)") + .attr("stroke-width", 2) + .attr("stroke-linecap", "round") + .attr("stroke-linejoin", "round") + .attr("d", line(data.slice(data.length - 2, data.length))) + } + + const totalDuration = 1_000 + const path1Duration = loadLastValue?.value ? (totalDuration / data.length) * (data.length - 1) : totalDuration + const path1Length = path1.node().getTotalLength() + + path1 + .attr("stroke-dasharray", path1Length) + .attr("stroke-dashoffset", path1Length) + .transition() + .duration(path1Duration) + .ease(d3.easeLinear) + .attr("stroke-dashoffset", 0) + + if (loadLastValue?.value) { + const path2Duration = totalDuration / data.length + const path2Length = path2.node().getTotalLength() + 1 + + path2 + .attr("stroke-dasharray", path2Length) + .attr("stroke-dashoffset", path2Length) + .transition() + .duration(path2Duration) + .ease(d3.easeLinear) + .delay(path1Duration) + .attr("stroke-dashoffset", 0) + } + + const point = svg + .append("circle") + .attr("cx", x(data[data.length - 1].date)) + .attr("cy", y(data[data.length - 1].value)) + .attr("fill", "var(--brand)") + .attr("r", 3) + .attr("opacity", 0) + + point.transition().delay(totalDuration).duration(200).attr("opacity", 1) + } else { + svg.append("text") + .attr("x", width / 2) + .attr("y", height * 0.3) + .attr("text-anchor", "middle") + .attr("fill", "var(--op-20)") + .style("font-size", "14px") + .text("No data available for this rollup") + } + + if (chartEl.children[0]) chartEl.children[0].remove() + chartEl.append(svg.node()) +} diff --git a/services/utils/index.js b/services/utils/index.js index 0b9284c7..c41cb4f1 100644 --- a/services/utils/index.js +++ b/services/utils/index.js @@ -2,3 +2,4 @@ export * from "./general" export * from "./amounts" export * from "./strings" export * from "./d3" +export * from "./charts" From 74529056f8327bbc9dbb57034579470cdc4d1b20 Mon Sep 17 00:00:00 2001 From: sstark21 Date: Fri, 15 Aug 2025 13:07:55 +0100 Subject: [PATCH 03/13] refactor: optimize bar charts --- components/modules/address/AddressCharts.vue | 157 +------------- .../modules/namespace/NamespaceCharts.vue | 144 +------------ components/modules/rollup/RollupCharts.vue | 198 +++--------------- services/config.js | 8 +- services/utils/charts.js | 186 ++++++++++++++++ 5 files changed, 223 insertions(+), 470 deletions(-) diff --git a/components/modules/address/AddressCharts.vue b/components/modules/address/AddressCharts.vue index e487018c..239ea372 100644 --- a/components/modules/address/AddressCharts.vue +++ b/components/modules/address/AddressCharts.vue @@ -12,7 +12,7 @@ import Toggle from "@/components/ui/Toggle.vue" /** Services */ import { abbreviate, tia } from "@/services/utils" -import { buildLineChart } from '@/services/utils/charts' +import { buildLineChart, buildBarChart } from "@/services/utils/charts" /** API */ import { fetchAddressSeries } from "@/services/api/stats" @@ -104,151 +104,6 @@ const badgeEl = ref() const badgeText = ref("") const badgeOffset = ref(0) -const buildBarChart = (chartEl, data, onEnter, onLeave, metric) => { - const width = chartWrapperEl.value.wrapper.getBoundingClientRect().width - const height = 180 - const marginTop = 0 - const marginRight = 2 - const marginBottom = 24 - const marginLeft = 52 - - const barWidth = Math.max(Math.round((width - marginLeft - marginRight) / data.length - (data.length > 7 ? 4 : 8)), 4) - - const MAX_VALUE = d3.max(data, (d) => d.value) ? d3.max(data, (d) => d.value) : 1 - - /** Scale */ - const x = d3.scaleUtc( - d3.extent(data, (d) => d.date), - [marginLeft, width - marginRight - barWidth], - ) - const y = d3.scaleLinear([0, MAX_VALUE], [height - marginBottom, marginTop]) - - /** Tooltip */ - const bisect = d3.bisector((d) => d.date).center - const onPointermoved = (event) => { - onEnter() - - const idx = bisect(data, x.invert(d3.pointer(event)[0] - barWidth / 2)) - - const elements = document.querySelectorAll(`[metric="${metric}"]`) - elements.forEach((el) => { - if (+el.getAttribute("data-index") === idx) { - el.style.filter = "brightness(1.2)" - } else { - el.style.filter = "brightness(0.6)" - } - }) - - tooltipXOffset.value = x(data[idx].date) - tooltipYDataOffset.value = y(data[idx].value) - tooltipYOffset.value = event.layerY - tooltipText.value = data[idx].value - - if (tooltipEl.value) { - if (idx > parseInt(selectedPeriod.value.value / 2)) { - tooltipDynamicXPosition.value = tooltipXOffset.value - tooltipEl.value.wrapper.getBoundingClientRect().width - 16 - } else { - tooltipDynamicXPosition.value = tooltipXOffset.value + 16 - } - } - - let tf = selectedPeriod.value.timeframe - if (metric === "tvl" && ["hour", "week"].includes(selectedPeriod.value.timeframe)) { - tf = "day" - } - badgeText.value = - tf === "month" - ? DateTime.fromJSDate(data[idx].date).toFormat("LLL") - : tf === "day" - ? DateTime.fromJSDate(data[idx].date).toFormat("LLL dd") - : DateTime.fromJSDate(data[idx].date).set({ minutes: 0 }).toFormat("hh:mm a") - - if (!badgeEl.value) return - const badgeWidth = badgeEl.value.getBoundingClientRect().width - if (tooltipXOffset.value - marginLeft < badgeWidth / 2) { - badgeOffset.value = 0 - } else if (badgeWidth + tooltipXOffset.value > width) { - badgeOffset.value = Math.abs(width - (badgeWidth + tooltipXOffset.value)) + (data.length - 1 - idx) * 2 - } else { - badgeOffset.value = (badgeWidth - barWidth) / 2 - } - } - const onPointerleft = () => { - onLeave() - - const elements = document.querySelectorAll("[data-index]") - elements.forEach((el) => { - el.style.filter = "" - }) - badgeText.value = "" - } - - /** SVG Container */ - const svg = d3 - .create("svg") - .attr("width", width) - .attr("height", height) - .attr("viewBox", [0, 0, width, height]) - .attr("preserveAspectRatio", "none") - .attr("style", "max-width: 100%; height: intrinsic;") - .style("-webkit-tap-highlight-color", "transparent") - .on("pointerenter pointermove", onPointermoved) - .on("pointerleave", onPointerleft) - .on("touchstart", (event) => event.preventDefault()) - - /** Vertical Lines */ - svg.append("path") - .attr("fill", "none") - .attr("stroke", "var(--op-10)") - .attr("stroke-width", 2) - .attr("d", `M${marginLeft},${height - marginBottom + 2} L${marginLeft},${height - marginBottom - 5}`) - svg.append("path") - .attr("fill", "none") - .attr("stroke", "var(--op-10)") - .attr("stroke-width", 2) - .attr("d", `M${width - 1},${height - marginBottom + 2} L${width - 1},${height - marginBottom - 5}`) - - /** Default Horizontal Line */ - svg.append("path") - .attr("fill", "none") - .attr("stroke", "var(--op-10)") - .attr("stroke-width", 2) - .attr("d", `M${0},${height - marginBottom - 6} L${width},${height - marginBottom - 6}`) - - /** Chart Bars */ - svg.append("defs") - .append("pattern") - .attr("id", "diagonal-stripe") - .attr("width", 6) - .attr("height", 6) - .attr("patternUnits", "userSpaceOnUse") - .attr("patternTransform", "rotate(45)") - .append("rect") - .attr("width", 2) - .attr("height", 6) - .attr("transform", "translate(0,0)") - .attr("fill", "var(--brand)") - - svg.append("g") - .selectAll("g") - .data(data) - .enter() - .append("rect") - .attr("class", "bar") - .attr("data-index", (d, i) => i) - .attr("metric", metric) - .attr("x", (d) => x(new Date(d.date))) - .attr("y", (d) => y(d.value)) - .attr("width", barWidth) - .attr("fill", (d, i) => (loadLastValue.value && i === data.length - 1 ? `url(#diagonal-stripe)` : "var(--brand)")) - .transition() - .duration(1_000) - .attr("height", (d) => Math.max(height - marginBottom - 6 - y(d.value), 0)) - - if (chartEl.children[0]) chartEl.children[0].remove() - chartEl.append(svg.node()) -} - const fetchData = async (metric) => { const data = await fetchAddressSeries({ hash: props.hash, @@ -335,7 +190,7 @@ const getFeeSeries = async () => { } } -const buildCharts = async (loadData = true) => { +const buildAddressCharts = async (loadData = true) => { isLoading.value = true if (loadData) { await getTxSeries() @@ -378,7 +233,7 @@ const buildCharts = async (loadData = true) => { watch( () => selectedPeriodIdx.value, () => { - buildCharts() + buildAddressCharts() }, ) @@ -387,13 +242,13 @@ watch( () => { updateUserSettings() if (!isLoading.value) { - buildCharts(false) + buildAddressCharts(false) } }, ) const debouncedRedraw = useDebounceFn((e) => { - buildCharts() + buildAddressCharts() }, 500) onBeforeMount(() => { @@ -406,7 +261,7 @@ onBeforeMount(() => { onMounted(async () => { window.addEventListener("resize", debouncedRedraw) - buildCharts() + buildAddressCharts() }) onBeforeUnmount(() => { diff --git a/components/modules/namespace/NamespaceCharts.vue b/components/modules/namespace/NamespaceCharts.vue index 4f2af529..3145f63a 100644 --- a/components/modules/namespace/NamespaceCharts.vue +++ b/components/modules/namespace/NamespaceCharts.vue @@ -12,7 +12,7 @@ import Toggle from "@/components/ui/Toggle.vue" /** Services */ import { abbreviate, formatBytes } from "@/services/utils" -import { buildLineChart } from "~/services/utils/charts" +import { buildLineChart, buildBarChart } from "@/services/utils/charts" /** API */ import { fetchNamespaceSeries } from "@/services/api/stats" @@ -135,147 +135,6 @@ const xAxisLabels = computed(() => { return labels }) -const buildBarChart = (chartEl, data, onEnter, onLeave, metric) => { - const width = chartWrapperEl.value.wrapper.getBoundingClientRect().width - const height = 180 - const marginTop = 0 - const marginRight = 2 - const marginBottom = 24 - const marginLeft = 52 - - const barWidth = Math.max(Math.round((width - marginLeft - marginRight) / data.length - (data.length > 7 ? 4 : 8)), 4) - - const MAX_VALUE = d3.max(data, (d) => d.value) ? d3.max(data, (d) => d.value) : 1 - - /** Scale */ - const x = d3.scaleUtc( - d3.extent(data, (d) => d.date), - [marginLeft, width - marginRight - barWidth], - ) - const y = d3.scaleLinear([0, MAX_VALUE], [height - marginBottom, marginTop]) - - /** Tooltip */ - const bisect = d3.bisector((d) => d.date).center - const onPointermoved = (event) => { - onEnter() - - const idx = bisect(data, x.invert(d3.pointer(event)[0] - barWidth / 2)) - - const elements = document.querySelectorAll(`[metric="${metric}"]`) - elements.forEach((el) => { - if (+el.getAttribute("data-index") === idx) { - el.style.filter = "brightness(1.2)" - } else { - el.style.filter = "brightness(0.6)" - } - }) - - tooltipXOffset.value = x(data[idx].date) - tooltipYDataOffset.value = y(data[idx].value) - tooltipYOffset.value = event.layerY - tooltipText.value = data[idx].value - - if (tooltipEl.value) { - if (idx > parseInt(selectedPeriod.value.value / 2)) { - tooltipDynamicXPosition.value = tooltipXOffset.value - tooltipEl.value.wrapper.getBoundingClientRect().width - 16 - } else { - tooltipDynamicXPosition.value = tooltipXOffset.value + 16 - } - } - - badgeText.value = - selectedPeriod.value.timeframe === "month" - ? DateTime.fromJSDate(data[idx].date).toFormat("LLL") - : selectedPeriod.value.timeframe === "day" - ? DateTime.fromJSDate(data[idx].date).toFormat("LLL dd") - : DateTime.fromJSDate(data[idx].date).set({ minutes: 0 }).toFormat("hh:mm a") - - if (!badgeEl.value) return - const badgeWidth = badgeEl.value.getBoundingClientRect().width - if (tooltipXOffset.value - marginLeft < badgeWidth / 2) { - badgeOffset.value = 0 - } else if (badgeWidth + tooltipXOffset.value > width) { - badgeOffset.value = Math.abs(width - (badgeWidth + tooltipXOffset.value)) + (data.length - 1 - idx) * 2 - } else { - badgeOffset.value = (badgeWidth - barWidth) / 2 - } - } - const onPointerleft = () => { - onLeave() - - const elements = document.querySelectorAll("[data-index]") - elements.forEach((el) => { - el.style.filter = "" - }) - badgeText.value = "" - } - - /** SVG Container */ - const svg = d3 - .create("svg") - .attr("width", width) - .attr("height", height) - .attr("viewBox", [0, 0, width, height]) - .attr("preserveAspectRatio", "none") - .attr("style", "max-width: 100%; height: intrinsic;") - .style("-webkit-tap-highlight-color", "transparent") - .on("pointerenter pointermove", onPointermoved) - .on("pointerleave", onPointerleft) - .on("touchstart", (event) => event.preventDefault()) - - /** Vertical Lines */ - svg.append("path") - .attr("fill", "none") - .attr("stroke", "var(--op-10)") - .attr("stroke-width", 2) - .attr("d", `M${marginLeft},${height - marginBottom + 2} L${marginLeft},${height - marginBottom - 5}`) - svg.append("path") - .attr("fill", "none") - .attr("stroke", "var(--op-10)") - .attr("stroke-width", 2) - .attr("d", `M${width - 1},${height - marginBottom + 2} L${width - 1},${height - marginBottom - 5}`) - - /** Default Horizontal Line */ - svg.append("path") - .attr("fill", "none") - .attr("stroke", "var(--op-10)") - .attr("stroke-width", 2) - .attr("d", `M${0},${height - marginBottom - 6} L${width},${height - marginBottom - 6}`) - - /** Chart Bars */ - svg.append("defs") - .append("pattern") - .attr("id", "diagonal-stripe") - .attr("width", 6) - .attr("height", 6) - .attr("patternUnits", "userSpaceOnUse") - .attr("patternTransform", "rotate(45)") - .append("rect") - .attr("width", 2) - .attr("height", 6) - .attr("transform", "translate(0,0)") - .attr("fill", "var(--brand)") - - svg.append("g") - .selectAll("g") - .data(data) - .enter() - .append("rect") - .attr("class", "bar") - .attr("data-index", (d, i) => i) - .attr("metric", metric) - .attr("x", (d) => x(new Date(d.date))) - .attr("y", (d) => y(d.value)) - .attr("width", barWidth) - .attr("fill", (d, i) => (loadLastValue.value && i === data.length - 1 ? `url(#diagonal-stripe)` : "var(--brand)")) - .transition() - .duration(1_000) - .attr("height", (d) => Math.max(height - marginBottom - 6 - y(d.value), 0)) - - if (chartEl.children[0]) chartEl.children[0].remove() - chartEl.append(svg.node()) -} - const fetchData = async (metric, from) => { const data = await fetchNamespaceSeries({ id: props.id, @@ -294,6 +153,7 @@ const fetchData = async (metric, from) => { return data } + const getSizeSeries = async () => { sizeSeries.value = [] diff --git a/components/modules/rollup/RollupCharts.vue b/components/modules/rollup/RollupCharts.vue index 36597dad..ec2c8718 100644 --- a/components/modules/rollup/RollupCharts.vue +++ b/components/modules/rollup/RollupCharts.vue @@ -14,7 +14,7 @@ import Tooltip from "@/components/ui/Tooltip.vue" /** Services */ import { abbreviate, formatBytes, sortArrayOfObjects, spaces, tia } from "@/services/utils" -import { buildLineChart } from "@/services/utils/charts" +import { buildLineChart, buildBarChart } from "@/services/utils/charts" /** API */ import { fetchRollupSeries } from "@/services/api/stats" @@ -158,172 +158,6 @@ const getXAxisLabels = (start, tvl = false) => { return res } -const buildBarChart = (chartEl, data, onEnter, onLeave, metric) => { - const width = chartWrapperEl.value.wrapper.getBoundingClientRect().width - const height = 180 - const marginTop = 0 - const marginRight = 2 - const marginBottom = 24 - const marginLeft = 52 - - const barWidth = Math.max(Math.round((width - marginLeft - marginRight) / data.length - (data.length > 7 ? 4 : 8)), 4) - - const MAX_VALUE = d3.max(data, (d) => d.value) ? d3.max(data, (d) => d.value) : 1 - - /** Scale */ - const x = d3 - .scaleBand() - .domain(data.map(d => d.date)) - .range([marginLeft, width - marginRight]) - .padding(0.1) - - const scaleX = d3.scaleUtc( - d3.extent(data, (d) => d.date), - [marginLeft, width - marginRight - barWidth], - ) - - const y = d3.scaleLinear([0, MAX_VALUE], [height - marginBottom, marginTop]) - - /** Tooltip */ - const bisect = d3.bisector((d) => d.date).center - const onPointermoved = (event) => { - if (!data.length) return - - onEnter() - - const idx = bisect(data, scaleX.invert(d3.pointer(event)[0] - barWidth / 2)) - - const elements = document.querySelectorAll(`[metric="${metric}"]`) - elements.forEach((el) => { - if (+el.getAttribute("data-index") === idx) { - el.style.filter = "brightness(1.2)" - } else { - el.style.filter = "brightness(0.6)" - } - }) - - tooltipXOffset.value = scaleX(data[idx].date) - tooltipYDataOffset.value = y(data[idx].value) - tooltipYOffset.value = event.layerY - tooltipText.value = data[idx].value - - if (tooltipEl.value) { - if (idx > parseInt(selectedPeriod.value.value / 2)) { - tooltipDynamicXPosition.value = tooltipXOffset.value - tooltipEl.value.wrapper.getBoundingClientRect().width - 16 - } else { - tooltipDynamicXPosition.value = tooltipXOffset.value + 16 - } - } - - let tf = selectedPeriod.value.timeframe - if (metric === "tvl" && ["hour", "week"].includes(selectedPeriod.value.timeframe)) { - tf = "day" - } - badgeText.value = - tf === "month" - ? DateTime.fromJSDate(data[idx].date).toFormat("LLL") - : tf === "day" - ? DateTime.fromJSDate(data[idx].date).toFormat("LLL dd") - : DateTime.fromJSDate(data[idx].date).set({ minutes: 0 }).toFormat("hh:mm a") - - if (!badgeEl.value) return - const badgeWidth = badgeEl.value.getBoundingClientRect().width - if (tooltipXOffset.value - marginLeft < badgeWidth / 2) { - badgeOffset.value = 0 - } else if (badgeWidth + tooltipXOffset.value > width) { - badgeOffset.value = Math.abs(width - (badgeWidth + tooltipXOffset.value)) + (data.length - 1 - idx) * 2 - } else { - badgeOffset.value = (badgeWidth - barWidth) / 2 - } - } - const onPointerleft = () => { - if (!data.length) return - - onLeave() - - const elements = document.querySelectorAll("[data-index]") - elements.forEach((el) => { - el.style.filter = "" - }) - badgeText.value = "" - } - - /** SVG Container */ - const svg = d3 - .create("svg") - .attr("width", width) - .attr("height", height) - .attr("viewBox", [0, 0, width, height]) - .attr("preserveAspectRatio", "none") - .attr("style", "max-width: 100%; height: intrinsic;") - .style("-webkit-tap-highlight-color", "transparent") - .on("pointerenter pointermove", onPointermoved) - .on("pointerleave", onPointerleft) - .on("touchstart", (event) => event.preventDefault()) - - /** Vertical Lines */ - svg.append("path") - .attr("fill", "none") - .attr("stroke", "var(--op-10)") - .attr("stroke-width", 2) - .attr("d", `M${marginLeft},${height - marginBottom + 2} L${marginLeft},${height - marginBottom - 5}`) - svg.append("path") - .attr("fill", "none") - .attr("stroke", "var(--op-10)") - .attr("stroke-width", 2) - .attr("d", `M${width - 1},${height - marginBottom + 2} L${width - 1},${height - marginBottom - 5}`) - - /** Default Horizontal Line */ - svg.append("path") - .attr("fill", "none") - .attr("stroke", "var(--op-10)") - .attr("stroke-width", 2) - .attr("d", `M${0},${height - marginBottom - 6} L${width},${height - marginBottom - 6}`) - - if (data.length) { - /** Chart Bars */ - svg.append("defs") - .append("pattern") - .attr("id", "diagonal-stripe") - .attr("width", 6) - .attr("height", 6) - .attr("patternUnits", "userSpaceOnUse") - .attr("patternTransform", "rotate(45)") - .append("rect") - .attr("width", 2) - .attr("height", 6) - .attr("transform", "translate(0,0)") - .attr("fill", "var(--brand)") - - svg.append("g") - .selectAll("g") - .data(data) - .enter() - .append("rect") - .attr("class", "bar") - .attr("data-index", (d, i) => i) - .attr("metric", metric) - .attr("x", (d) => x(new Date(d.date))) - .attr("y", (d) => y(d.value)) - .attr("width", x.bandwidth()) - .attr("fill", (d, i) => (loadLastValue.value && i === data.length - 1 ? `url(#diagonal-stripe)` : "var(--brand)")) - .transition() - .duration(1_000) - .attr("height", (d) => Math.max(height - marginBottom - 6 - y(d.value), 0)) - } else { - svg.append("text") - .attr("x", width / 2) - .attr("y", height * 0.3) - .attr("text-anchor", "middle") - .attr("fill", "var(--op-20)") - .style("font-size", "14px") - .text("No data available for this rollup") - } - - if (chartEl.children[0]) chartEl.children[0].remove() - chartEl.append(svg.node()) -} - const getRollupsList = async () => { const data = await fetchRollups({ limit: 30, @@ -713,10 +547,10 @@ onMounted(async () => { window.addEventListener("resize", debouncedRedraw) if (props.rollup["l2_beat"]) { - tvlDataSources.value.push({ name: "l2beat", title: "L2Beat"}) + tvlDataSources.value.push({ name: "l2beat", title: "L2Beat" }) } if (props.rollup["defi_lama"]) { - tvlDataSources.value.push({ name: "llama", title: "Defi Llama"}) + tvlDataSources.value.push({ name: "llama", title: "Defi Llama" }) } selectedTvlDataSource.value = tvlDataSources.value[0] @@ -1085,7 +919,14 @@ onBeforeUnmount(() => { TVL - {{ `${abbreviate(tvlSeries[tvlSeries.length - 1].value ? tvlSeries[tvlSeries.length - 1].value : tvlSeries[tvlSeries.length - 2].value, 2)} USD` }} + {{ + `${abbreviate( + tvlSeries[tvlSeries.length - 1].value + ? tvlSeries[tvlSeries.length - 1].value + : tvlSeries[tvlSeries.length - 2].value, + 2, + )} USD` + }} @@ -1093,13 +934,21 @@ onBeforeUnmount(() => { - + { name="chevron" size="14" color="secondary" - :style="{ transform: `rotate(${isTvlDataSourcePopoverOpen ? '180' : '0'}deg)`, transition: 'all 0.25s ease' }" + :style="{ + transform: `rotate(${isTvlDataSourcePopoverOpen ? '180' : '0'}deg)`, + transition: 'all 0.25s ease', + }" /> diff --git a/services/config.js b/services/config.js index b625fe45..8854c104 100644 --- a/services/config.js +++ b/services/config.js @@ -39,10 +39,10 @@ export const useServerURL = () => { return Server.API.mammoth case "dev.celenium.io": - return Server.API.dev + return Server.API.mainnet default: - return Server.API.dev + return Server.API.mainnet } } @@ -66,10 +66,10 @@ export const useSocketURL = () => { return Server.WSS.mammoth case "dev.celenium.io": - return Server.WSS.dev + return Server.WSS.mainnet default: - return Server.WSS.dev + return Server.WSS.mainnet } } diff --git a/services/utils/charts.js b/services/utils/charts.js index 3835e5e4..99cfa7b4 100644 --- a/services/utils/charts.js +++ b/services/utils/charts.js @@ -218,3 +218,189 @@ export const buildLineChart = (chartEl, data, onEnter, onLeave, metric) => { if (chartEl.children[0]) chartEl.children[0].remove() chartEl.append(svg.node()) } + +/** + * Builds a bar chart using D3.js + * @param {HTMLElement} chartEl - DOM element for chart placement + * @param {Array} data - Data for the chart [{date, value}] + * @param {Function} onEnter - Callback on hover + * @param {Function} onLeave - Callback on cursor leave + * @param {string} metric - Metric for chart identification + */ +export const buildBarChart = (chartEl, data, onEnter, onLeave, metric) => { + const width = chartEl.parentElement.getBoundingClientRect().width + const height = 180 + const marginTop = 0 + const marginRight = 2 + const marginBottom = 24 + const marginLeft = 52 + + const barWidth = Math.max(Math.round((width - marginLeft - marginRight) / data.length - (data.length > 7 ? 4 : 8)), 4) + + const MAX_VALUE = d3.max(data, (d) => d.value) ? d3.max(data, (d) => d.value) : 1 + + /** Scale */ + const x = d3.scaleUtc( + d3.extent(data, (d) => d.date), + [marginLeft, width - marginRight - barWidth], + ) + const y = d3.scaleLinear([0, MAX_VALUE], [height - marginBottom, marginTop]) + + /** Tooltip */ + const bisect = d3.bisector((d) => d.date).center + const onPointermoved = (event) => { + if (!data.length) return + + onEnter() + + const idx = bisect(data, x.invert(d3.pointer(event)[0] - barWidth / 2)) + + const elements = document.querySelectorAll(`[metric="${metric}"]`) + elements.forEach((el) => { + if (+el.getAttribute("data-index") === idx) { + el.style.filter = "brightness(1.2)" + } else { + el.style.filter = "brightness(0.6)" + } + }) + + // Get tooltip elements from window object + const tooltipXOffset = window.currentTooltipXOffset + const tooltipYDataOffset = window.currentTooltipYDataOffset + const tooltipYOffset = window.currentTooltipYOffset + const tooltipText = window.currentTooltipText + const tooltipDynamicXPosition = window.currentTooltipDynamicXPosition + const badgeText = window.currentBadgeText + const badgeOffset = window.currentBadgeOffset + const tooltipEl = window.currentTooltipEl + const badgeEl = window.currentBadgeEl + const selectedPeriod = window.currentSelectedPeriod + + if (tooltipXOffset) tooltipXOffset.value = x(data[idx].date) + if (tooltipYDataOffset) tooltipYDataOffset.value = y(data[idx].value) + if (tooltipYOffset) tooltipYOffset.value = event.layerY + if (tooltipText) tooltipText.value = data[idx].value + + if (tooltipEl && tooltipEl.value) { + if (idx > parseInt(selectedPeriod?.value?.value / 2)) { + tooltipDynamicXPosition.value = tooltipXOffset.value - tooltipEl.value.wrapper.getBoundingClientRect().width - 16 + } else { + tooltipDynamicXPosition.value = tooltipXOffset.value + 16 + } + } + + let tf = selectedPeriod?.value?.timeframe + if (metric === "tvl" && ["hour", "week"].includes(selectedPeriod?.value?.timeframe)) { + tf = "day" + } + + if (badgeText) { + badgeText.value = + tf === "month" + ? DateTime.fromJSDate(data[idx].date).toFormat("LLL") + : tf === "day" + ? DateTime.fromJSDate(data[idx].date).toFormat("LLL dd") + : DateTime.fromJSDate(data[idx].date).set({ minutes: 0 }).toFormat("hh:mm a") + } + + if (badgeEl && badgeEl.value) { + const badgeWidth = badgeEl.value.getBoundingClientRect().width + if (tooltipXOffset.value - marginLeft < badgeWidth / 2) { + badgeOffset.value = 0 + } else if (badgeWidth + tooltipXOffset.value > width) { + badgeOffset.value = Math.abs(width - (badgeWidth + tooltipXOffset.value)) + (data.length - 1 - idx) * 2 + } else { + badgeOffset.value = (badgeWidth - barWidth) / 2 + } + } + } + + const onPointerleft = () => { + if (!data.length) return + + onLeave() + + const elements = document.querySelectorAll("[data-index]") + elements.forEach((el) => { + el.style.filter = "" + }) + + const badgeText = window.currentBadgeText + if (badgeText) badgeText.value = "" + } + + /** SVG Container */ + const svg = d3 + .create("svg") + .attr("width", width) + .attr("height", height) + .attr("viewBox", [0, 0, width, height]) + .attr("preserveAspectRatio", "none") + .attr("style", "max-width: 100%; height: intrinsic;") + .style("-webkit-tap-highlight-color", "transparent") + .on("pointerenter pointermove", onPointermoved) + .on("pointerleave", onPointerleft) + .on("touchstart", (event) => event.preventDefault()) + + /** Vertical Lines */ + svg.append("path") + .attr("fill", "none") + .attr("stroke", "var(--op-10)") + .attr("stroke-width", 2) + .attr("d", `M${marginLeft},${height - marginBottom + 2} L${marginLeft},${height - marginBottom - 5}`) + svg.append("path") + .attr("fill", "none") + .attr("stroke", "var(--op-10)") + .attr("stroke-width", 2) + .attr("d", `M${width - 1},${height - marginBottom + 2} L${width - 1},${height - marginBottom - 5}`) + + /** Default Horizontal Line */ + svg.append("path") + .attr("fill", "none") + .attr("stroke", "var(--op-10)") + .attr("stroke-width", 2) + .attr("d", `M${0},${height - marginBottom - 6} L${width},${height - marginBottom - 6}`) + + if (data.length) { + /** Chart Bars */ + svg.append("defs") + .append("pattern") + .attr("id", "diagonal-stripe") + .attr("width", 6) + .attr("height", 6) + .attr("patternUnits", "userSpaceOnUse") + .attr("patternTransform", "rotate(45)") + .append("rect") + .attr("width", 2) + .attr("height", 6) + .attr("transform", "translate(0,0)") + .attr("fill", "var(--brand)") + + svg.append("g") + .selectAll("g") + .data(data) + .enter() + .append("rect") + .attr("class", "bar") + .attr("data-index", (d, i) => i) + .attr("metric", metric) + .attr("x", (d) => x(new Date(d.date))) + .attr("y", (d) => y(d.value)) + .attr("width", barWidth) + .attr("fill", (d, i) => (loadLastValue?.value && i === data.length - 1 ? `url(#diagonal-stripe)` : "var(--brand)")) + .transition() + .duration(1_000) + .attr("height", (d) => Math.max(height - marginBottom - 6 - y(d.value), 0)) + } else { + svg.append("text") + .attr("x", width / 2) + .attr("y", height * 0.3) + .attr("text-anchor", "middle") + .attr("fill", "var(--op-20)") + .style("font-size", "14px") + .text("No data available for this rollup") + } + + if (chartEl.children[0]) chartEl.children[0].remove() + chartEl.append(svg.node()) +} From 1958431b2c8edb1ea4ae6d7adaead3b8910a10cf Mon Sep 17 00:00:00 2001 From: sstark21 Date: Tue, 19 Aug 2025 18:29:35 +0100 Subject: [PATCH 04/13] fix: bar charts --- services/utils/charts.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/utils/charts.js b/services/utils/charts.js index 99cfa7b4..2c9e4e70 100644 --- a/services/utils/charts.js +++ b/services/utils/charts.js @@ -362,6 +362,8 @@ export const buildBarChart = (chartEl, data, onEnter, onLeave, metric) => { .attr("d", `M${0},${height - marginBottom - 6} L${width},${height - marginBottom - 6}`) if (data.length) { + const loadLastValue = window.currentLoadLastValue + /** Chart Bars */ svg.append("defs") .append("pattern") From 07161a097de574713ba3f216eadbbda46993bf64 Mon Sep 17 00:00:00 2001 From: sstark21 Date: Wed, 20 Aug 2025 17:26:42 +0100 Subject: [PATCH 05/13] fixiki --- components/modules/rollup/RollupCharts.vue | 25 +++++++++ services/utils/charts.js | 61 +++++++++++----------- 2 files changed, 56 insertions(+), 30 deletions(-) diff --git a/components/modules/rollup/RollupCharts.vue b/components/modules/rollup/RollupCharts.vue index ec2c8718..7c6b307e 100644 --- a/components/modules/rollup/RollupCharts.vue +++ b/components/modules/rollup/RollupCharts.vue @@ -400,6 +400,21 @@ const filteredRollupsList = computed(() => { const buildRollupCharts = async (loadData = true) => { isLoading.value = true + + const tooltipConfig = { + tooltipXOffset, + tooltipYDataOffset, + tooltipYOffset, + tooltipText, + tooltipDynamicXPosition, + badgeText, + badgeOffset, + tooltipEl, + badgeEl, + selectedPeriod, + loadLastValue, + } + if (loadData) { await getRollupsList() if (!selectedRollup.value) { @@ -421,18 +436,23 @@ const buildRollupCharts = async (loadData = true) => { () => (showSeriesTooltip.value = true), () => (showSeriesTooltip.value = false), "size", + tooltipConfig ) buildLineChart( pfbSeriesChartEl.value.wrapper, loadLastValue.value ? pfbSeries.value : pfbSeries.value.slice(0, pfbSeries.value.length - 1), () => (showPfbTooltip.value = true), () => (showPfbTooltip.value = false), + "pfb", + tooltipConfig ) buildLineChart( feeSeriesChartEl.value.wrapper, loadLastValue.value ? feeSeries.value : feeSeries.value.slice(0, feeSeries.value.length - 1), () => (showFeeTooltip.value = true), () => (showFeeTooltip.value = false), + "fee", + tooltipConfig ) buildLineChart( tvlSeriesChartEl.value.wrapper, @@ -440,6 +460,7 @@ const buildRollupCharts = async (loadData = true) => { () => (showTVLTooltip.value = true), () => (showTVLTooltip.value = false), "tvl", + tooltipConfig ) } else { buildBarChart( @@ -448,6 +469,7 @@ const buildRollupCharts = async (loadData = true) => { () => (showSeriesTooltip.value = true), () => (showSeriesTooltip.value = false), "size", + tooltipConfig, ) buildBarChart( pfbSeriesChartEl.value.wrapper, @@ -455,6 +477,7 @@ const buildRollupCharts = async (loadData = true) => { () => (showPfbTooltip.value = true), () => (showPfbTooltip.value = false), "pfb", + tooltipConfig, ) buildBarChart( feeSeriesChartEl.value.wrapper, @@ -462,6 +485,7 @@ const buildRollupCharts = async (loadData = true) => { () => (showFeeTooltip.value = true), () => (showFeeTooltip.value = false), "fee", + tooltipConfig, ) buildBarChart( tvlSeriesChartEl.value.wrapper, @@ -469,6 +493,7 @@ const buildRollupCharts = async (loadData = true) => { () => (showTVLTooltip.value = true), () => (showTVLTooltip.value = false), "tvl", + tooltipConfig, ) } diff --git a/services/utils/charts.js b/services/utils/charts.js index 2c9e4e70..f0024877 100644 --- a/services/utils/charts.js +++ b/services/utils/charts.js @@ -9,7 +9,7 @@ import { DateTime } from "luxon" * @param {Function} onLeave - Callback on cursor leave * @param {string} metric - Metric (optional, for special TVL logic) */ -export const buildLineChart = (chartEl, data, onEnter, onLeave, metric) => { +export const buildLineChart = (chartEl, data, onEnter, onLeave, metric, tooltipConfig) => { const width = chartEl.parentElement.getBoundingClientRect().width const height = 180 const marginTop = 0 @@ -39,17 +39,18 @@ export const buildLineChart = (chartEl, data, onEnter, onLeave, metric) => { const idx = bisect(data, x.invert(d3.pointer(event)[0])) - const tooltipXOffset = window.currentTooltipXOffset - const tooltipYDataOffset = window.currentTooltipYDataOffset - const tooltipYOffset = window.currentTooltipYOffset - const tooltipText = window.currentTooltipText - const tooltipDynamicXPosition = window.currentTooltipDynamicXPosition - const badgeText = window.currentBadgeText - const badgeOffset = window.currentBadgeOffset - const tooltipEl = window.currentTooltipEl - const badgeEl = window.currentBadgeEl - const selectedPeriod = window.currentSelectedPeriod - const loadLastValue = window.currentLoadLastValue + const { + tooltipXOffset, + tooltipYDataOffset, + tooltipYOffset, + tooltipText, + tooltipDynamicXPosition, + badgeText, + badgeOffset, + tooltipEl, + badgeEl, + selectedPeriod, + } = tooltipConfig if (tooltipXOffset) tooltipXOffset.value = x(data[idx].date) if (tooltipYDataOffset) tooltipYDataOffset.value = y(data[idx].value) @@ -95,7 +96,7 @@ export const buildLineChart = (chartEl, data, onEnter, onLeave, metric) => { onLeave() - const badgeText = window.currentBadgeText + const { badgeText } = tooltipConfig if (badgeText) badgeText.value = "" } @@ -133,11 +134,10 @@ export const buildLineChart = (chartEl, data, onEnter, onLeave, metric) => { if (data.length) { /** Chart Line */ + const { loadLastValue } = tooltipConfig let path1 = null let path2 = null - const loadLastValue = window.currentLoadLastValue - path1 = svg .append("path") .attr("fill", "none") @@ -227,7 +227,7 @@ export const buildLineChart = (chartEl, data, onEnter, onLeave, metric) => { * @param {Function} onLeave - Callback on cursor leave * @param {string} metric - Metric for chart identification */ -export const buildBarChart = (chartEl, data, onEnter, onLeave, metric) => { +export const buildBarChart = (chartEl, data, onEnter, onLeave, metric, tooltipConfig) => { const width = chartEl.parentElement.getBoundingClientRect().width const height = 180 const marginTop = 0 @@ -235,7 +235,7 @@ export const buildBarChart = (chartEl, data, onEnter, onLeave, metric) => { const marginBottom = 24 const marginLeft = 52 - const barWidth = Math.max(Math.round((width - marginLeft - marginRight) / data.length - (data.length > 7 ? 4 : 8)), 4) + const barWidth = Math.max(Math.round((width - marginLeft - marginRight) / data.length - (data.length > 7 ? 2 : 8)), 3) const MAX_VALUE = d3.max(data, (d) => d.value) ? d3.max(data, (d) => d.value) : 1 @@ -264,17 +264,18 @@ export const buildBarChart = (chartEl, data, onEnter, onLeave, metric) => { } }) - // Get tooltip elements from window object - const tooltipXOffset = window.currentTooltipXOffset - const tooltipYDataOffset = window.currentTooltipYDataOffset - const tooltipYOffset = window.currentTooltipYOffset - const tooltipText = window.currentTooltipText - const tooltipDynamicXPosition = window.currentTooltipDynamicXPosition - const badgeText = window.currentBadgeText - const badgeOffset = window.currentBadgeOffset - const tooltipEl = window.currentTooltipEl - const badgeEl = window.currentBadgeEl - const selectedPeriod = window.currentSelectedPeriod + const { + tooltipXOffset, + tooltipYDataOffset, + tooltipYOffset, + tooltipText, + tooltipDynamicXPosition, + badgeText, + badgeOffset, + tooltipEl, + badgeEl, + selectedPeriod, + } = tooltipConfig if (tooltipXOffset) tooltipXOffset.value = x(data[idx].date) if (tooltipYDataOffset) tooltipYDataOffset.value = y(data[idx].value) @@ -325,7 +326,7 @@ export const buildBarChart = (chartEl, data, onEnter, onLeave, metric) => { el.style.filter = "" }) - const badgeText = window.currentBadgeText + const { badgeText } = tooltipConfig if (badgeText) badgeText.value = "" } @@ -362,7 +363,7 @@ export const buildBarChart = (chartEl, data, onEnter, onLeave, metric) => { .attr("d", `M${0},${height - marginBottom - 6} L${width},${height - marginBottom - 6}`) if (data.length) { - const loadLastValue = window.currentLoadLastValue + const { loadLastValue } = tooltipConfig /** Chart Bars */ svg.append("defs") From ef77b740e5697396714ad20243b5380f8260a3bb Mon Sep 17 00:00:00 2001 From: sstark21 Date: Fri, 22 Aug 2025 10:07:13 +0100 Subject: [PATCH 06/13] working draft --- components/modules/rollup/RollupCharts.vue | 1118 ++++------------- components/ui/ChartOnEntityPage.vue | 331 +++++ services/utils/{charts.js => entityCharts.js} | 27 +- services/utils/index.js | 2 +- 4 files changed, 592 insertions(+), 886 deletions(-) create mode 100644 components/ui/ChartOnEntityPage.vue rename services/utils/{charts.js => entityCharts.js} (94%) diff --git a/components/modules/rollup/RollupCharts.vue b/components/modules/rollup/RollupCharts.vue index 7c6b307e..69d087db 100644 --- a/components/modules/rollup/RollupCharts.vue +++ b/components/modules/rollup/RollupCharts.vue @@ -11,10 +11,14 @@ import Input from "@/components/ui/Input.vue" import Popover from "@/components/ui/Popover.vue" import Toggle from "@/components/ui/Toggle.vue" import Tooltip from "@/components/ui/Tooltip.vue" +import ChartOnEntityPage from "@/components/ui/ChartOnEntityPage.vue" +import Icon from "@/components/Icon.vue" +import Text from "@/components/Text.vue" +import Flex from "@/components/Flex.vue" /** Services */ import { abbreviate, formatBytes, sortArrayOfObjects, spaces, tia } from "@/services/utils" -import { buildLineChart, buildBarChart } from "@/services/utils/charts" +import { buildLineChart, buildBarChart } from "@/services/utils/entityCharts" /** API */ import { fetchRollupSeries } from "@/services/api/stats" @@ -55,6 +59,8 @@ const periods = ref([ timeframe: "month", }, ]) + + const selectedPeriod = computed(() => periods.value[selectedPeriodIdx.value]) const chartView = ref("line") const loadLastValue = ref(true) @@ -67,14 +73,6 @@ const handleClose = () => { isOpen.value = false } -const handleChangeChartView = () => { - if (chartView.value === "line") { - chartView.value = "bar" - } else { - chartView.value = "line" - } -} - const updateUserSettings = () => { settingsStore.chart = { ...settingsStore.chart, @@ -84,11 +82,6 @@ const updateUserSettings = () => { } /** Charts */ -const chartWrapperEl = ref() -const sizeSeriesChartEl = ref() -const pfbSeriesChartEl = ref() -const feeSeriesChartEl = ref() -const tvlSeriesChartEl = ref() const comparisonChartEl = ref() const comparisonBarWidth = ref(0) @@ -104,59 +97,13 @@ const rollupsList = ref() const comparisonData = ref([]) const selectedRollup = ref() -/** Tooltip */ -const showSeriesTooltip = ref(false) -const showPfbTooltip = ref(false) -const showFeeTooltip = ref(false) -const showTVLTooltip = ref(false) -const tooltipEl = ref() -const tooltipXOffset = ref(0) -const tooltipYOffset = ref(0) -const tooltipYDataOffset = ref(0) -const tooltipDynamicXPosition = ref(0) -const tooltipText = ref("") - -const badgeEl = ref() -const badgeText = ref("") -const badgeOffset = ref(0) - -const getXAxisLabels = (start, tvl = false) => { - let res = "" - - let tf = selectedPeriod.value.timeframe - let periodValue = selectedPeriod.value.value - if (tvl && ["hour", "week"].includes(selectedPeriod.value.timeframe)) { - tf = "day" - periodValue = 30 - } - - switch (tf) { - case "month": - start - ? (res = DateTime.now() - .minus({ months: periodValue - 1 }) - .toFormat("LLL y")) - : (res = loadLastValue.value ? DateTime.now().toFormat("LLL") : DateTime.now().minus({ months: 1 }).toFormat("LLL")) - break - case "day": - start - ? (res = DateTime.now() - .minus({ days: periodValue - 1 }) - .toFormat("LLL dd")) - : (res = loadLastValue.value ? "Today" : DateTime.now().minus({ days: 1 }).toFormat("LLL dd")) - break - default: - start - ? (res = DateTime.now() - .minus({ hours: periodValue - 1 }) - .set({ minutes: 0 }) - .toFormat("hh:mm a")) - : (res = loadLastValue.value ? "Now" : DateTime.now().minus({ hours: 1 }).set({ minutes: 0 }).toFormat("hh:mm a")) - break - } - - return res -} +/** Series config */ +const seriesConfig = [ + { name: "size", metric: "size", series: sizeSeries }, + { name: "blobs_count", metric: "pfb", series: pfbSeries }, + { name: "fee", metric: "fee", series: feeSeries }, + { name: "tvl", metric: "tvl", series: tvlSeries }, +] const getRollupsList = async () => { const data = await fetchRollups({ @@ -167,178 +114,135 @@ const getRollupsList = async () => { } const fetchData = async (rollup, metric, from, timeframe) => { - const data = await fetchRollupSeries({ - id: rollup.id, - name: metric, - timeframe: timeframe ? timeframe : selectedPeriod.value.timeframe, - from: from - ? from - : parseInt( - DateTime.now().minus({ - days: selectedPeriod.value.timeframe === "day" ? selectedPeriod.value.value : 0, - hours: selectedPeriod.value.timeframe === "hour" ? selectedPeriod.value.value : 0, - months: selectedPeriod.value.timeframe === "month" ? selectedPeriod.value.value : 0, - }).ts / 1_000, - ), - }) + if (metric === "tvl") { + let from = "" - return data -} -const getSizeSeries = async () => { - sizeSeries.value = [] + const { timeframe: tf, value: periodValue } = selectedPeriod.value - const sizeSeriesRawData = await fetchData(props.rollup, "size") + if (["hour", "week"].includes(tf)) { + from = parseInt(DateTime.now().minus({ days: 30 }).ts / 1_000) + tf = "day" + periodValue = 30 + } - const sizeSeriesMap = {} - sizeSeriesRawData.forEach((item) => { - sizeSeriesMap[ - DateTime.fromISO(item.time).toFormat(["day", "month"].includes(selectedPeriod.value.timeframe) ? "y-LL-dd" : "y-LL-dd-HH") - ] = item.value - }) + return await fetchRollupTVL({ + dataSource: selectedTvlDataSource.value?.name, + slug: props.rollup.slug, + period: tf, + from, + }) + } else { + return await fetchRollupSeries({ + id: rollup.id, + name: metric, + timeframe: timeframe ? timeframe : selectedPeriod.value.timeframe, + from: from + ? from + : parseInt( + DateTime.now().minus({ + days: selectedPeriod.value.timeframe === "day" ? selectedPeriod.value.value : 0, + hours: selectedPeriod.value.timeframe === "hour" ? selectedPeriod.value.value : 0, + months: selectedPeriod.value.timeframe === "month" ? selectedPeriod.value.value : 0, + }).ts / 1_000, + ), + }) + } +} - for (let i = 1; i < selectedPeriod.value.value + 1; i++) { - let dt - if (selectedPeriod.value.timeframe === "month") { - dt = DateTime.now() - .startOf("month") - .minus({ - months: selectedPeriod.value.timeframe === "month" ? selectedPeriod.value.value - i : 0, - }) - } else { - dt = DateTime.now().minus({ - days: selectedPeriod.value.timeframe === "day" ? selectedPeriod.value.value - i : 0, - hours: selectedPeriod.value.timeframe === "hour" ? selectedPeriod.value.value - i : 0, - }) - } - sizeSeries.value.push({ - date: dt.toJSDate(), - value: - parseInt( - sizeSeriesMap[dt.toFormat(["day", "month"].includes(selectedPeriod.value.timeframe) ? "y-LL-dd" : "y-LL-dd-HH")], - ) || 0, +const generateDateForPeriod = (period, index) => { + const { timeframe, value } = period + + if (timeframe === "month") { + return DateTime.now() + .startOf("month") + .minus({ months: value - index }) + } else { + return DateTime.now().minus({ + [timeframe === "day" ? "days" : "hours"]: value - index, }) } } -const getPfbSeries = async () => { - pfbSeries.value = [] +const getFormatKey = (timeframe) => { + return ["day", "month"].includes(timeframe) ? "y-LL-dd" : "y-LL-dd-HH" +} - const blobsSeriesRawData = await fetchData(props.rollup, "blobs_count") +const generateSeriesData = (period, dataMap, series) => { + series.value = [] - const blobsSeriesMap = {} - blobsSeriesRawData.forEach((item) => { - blobsSeriesMap[ - DateTime.fromISO(item.time).toFormat(["day", "month"].includes(selectedPeriod.value.timeframe) ? "y-LL-dd" : "y-LL-dd-HH") - ] = item.value - }) + for (let i = 1; i < period.value + 1; i++) { + const dt = generateDateForPeriod(period, i) + const formatKey = getFormatKey(period.timeframe) + const key = dt.toFormat(formatKey) - for (let i = 1; i < selectedPeriod.value.value + 1; i++) { - let dt - if (selectedPeriod.value.timeframe === "month") { - dt = DateTime.now() - .startOf("month") - .minus({ - months: selectedPeriod.value.timeframe === "month" ? selectedPeriod.value.value - i : 0, - }) - } else { - dt = DateTime.now().minus({ - days: selectedPeriod.value.timeframe === "day" ? selectedPeriod.value.value - i : 0, - hours: selectedPeriod.value.timeframe === "hour" ? selectedPeriod.value.value - i : 0, - }) - } - pfbSeries.value.push({ + series.value.push({ date: dt.toJSDate(), - value: - parseInt( - blobsSeriesMap[dt.toFormat(["day", "month"].includes(selectedPeriod.value.timeframe) ? "y-LL-dd" : "y-LL-dd-HH")], - ) || 0, + value: parseInt(dataMap[key]) || 0, }) } } -const getFeeSeries = async () => { - feeSeries.value = [] +const generateTVLSeriesData = (period, dataMap, series, tvl = false) => { + series.value = [] - const feeSeriesRawData = await fetchData(props.rollup, "fee") + let tf = period.timeframe + let periodValue = period.value - const feeSeriesMap = {} - feeSeriesRawData.forEach((item) => { - feeSeriesMap[ - DateTime.fromISO(item.time).toFormat(["day", "month"].includes(selectedPeriod.value.timeframe) ? "y-LL-dd" : "y-LL-dd-HH") - ] = item.value - }) + if (tvl && ["hour", "week"].includes(period.timeframe)) { + tf = "day" + periodValue = 30 + } - for (let i = 1; i < selectedPeriod.value.value + 1; i++) { - let dt - if (selectedPeriod.value.timeframe === "month") { - dt = DateTime.now() - .startOf("month") - .minus({ - months: selectedPeriod.value.timeframe === "month" ? selectedPeriod.value.value - i : 0, - }) - } else { - dt = DateTime.now().minus({ - days: selectedPeriod.value.timeframe === "day" ? selectedPeriod.value.value - i : 0, - hours: selectedPeriod.value.timeframe === "hour" ? selectedPeriod.value.value - i : 0, - }) - } - feeSeries.value.push({ + for (let i = 1; i < periodValue + 1; i++) { + const dt = generateDateForPeriod({ timeframe: tf, value: periodValue }, i) + const formatKey = getFormatKey(tf) + const key = dt.toFormat(formatKey) + + series.value.push({ date: dt.toJSDate(), - value: - parseInt(feeSeriesMap[dt.toFormat(["day", "month"].includes(selectedPeriod.value.timeframe) ? "y-LL-dd" : "y-LL-dd-HH")]) || - 0, + value: parseFloat(dataMap[key]) || 0, }) } } -const getTVLSeries = async () => { - tvlSeries.value = [] - if (!selectedTvlDataSource.value?.name) return +const createDataMap = (rawData, timeframe) => { + const dataMap = {} + const formatKey = getFormatKey(timeframe) + + rawData.forEach((item) => { + dataMap[DateTime.fromISO(item.time).toFormat(formatKey)] = item.value + }) + + return dataMap +} +const generateSeries = async (configs) => { isLoading.value = true - let from = "" - let tf = selectedPeriod.value.timeframe - let periodValue = selectedPeriod.value.value - if (["hour", "week"].includes(selectedPeriod.value.timeframe)) { - from = parseInt(DateTime.now().minus({ days: 30 }).ts / 1_000) - tf = "day" - periodValue = 30 + + /** Get comparison chart width */ + comparisonBarWidth.value = comparisonChartEl.value.wrapper.getBoundingClientRect().width + await getRollupsList() + if (!selectedRollup.value) { + selectedRollup.value = rollupsList.value[0] } - const tvlSeriesRawData = await fetchRollupTVL({ - dataSource: selectedTvlDataSource.value?.name, - slug: props.rollup.slug, - period: tf, - from, - }) - const tvlSeriesMap = {} - tvlSeriesRawData.forEach((item) => { - tvlSeriesMap[DateTime.fromISO(item.time).toFormat(["day", "month"].includes(tf) ? "y-LL-dd" : "y-LL-dd-HH")] = item.value - }) + await Promise.all( + configs.map(async (config) => { + const rawData = await fetchData(props.rollup, config.name) + const dataMap = createDataMap(rawData, selectedPeriod.value.timeframe) - for (let i = 1; i < periodValue + 1; i++) { - let dt - if (tf === "month") { - dt = DateTime.now() - .startOf("month") - .minus({ - months: tf === "month" ? periodValue - i : 0, - }) - } else { - dt = DateTime.now().minus({ - days: tf === "day" ? periodValue - i : 0, - hours: tf === "hour" ? periodValue - i : 0, - }) - } - tvlSeries.value.push({ - date: dt.toJSDate(), - value: parseFloat(tvlSeriesMap[dt.toFormat(["day", "month"].includes(tf) ? "y-LL-dd" : "y-LL-dd-HH")]) || 0, - }) - } + if (config.metric === "tvl") { + generateTVLSeriesData(selectedPeriod.value, dataMap, config.series, true) + } else { + generateSeriesData(selectedPeriod.value, dataMap, config.series) + } - isLoading.value = false + isLoading.value = false + }), + ) } + const isTvlDataSourcePopoverOpen = ref(false) const handleTvlDataSourcePopoverClose = () => { isTvlDataSourcePopoverOpen.value = false @@ -350,6 +254,7 @@ const handleSelectTvlDataSource = (ds) => { const prepareComparisonData = async () => { isLoading.value = true + comparisonData.value[1] = {} if (!comparisonData.value[0]?.fee) { comparisonData.value[0] = { @@ -373,6 +278,7 @@ const prepareComparisonData = async () => { let firstRollup = comparisonData.value[0] let secondRollup = comparisonData.value[1] + Object.keys(firstRollup).forEach((el) => { let sum = firstRollup[el] + secondRollup[el] firstRollup[el + "_graph"] = Math.max(Math.round((firstRollup[el] / sum) * 100, 2), 1) @@ -397,118 +303,25 @@ const filteredRollupsList = computed(() => { return rollupsList.value.filter((r) => r.name.toLowerCase().includes(searchTerm.value.trim().toLowerCase())) }) -const buildRollupCharts = async (loadData = true) => { - isLoading.value = true - - - const tooltipConfig = { - tooltipXOffset, - tooltipYDataOffset, - tooltipYOffset, - tooltipText, - tooltipDynamicXPosition, - badgeText, - badgeOffset, - tooltipEl, - badgeEl, - selectedPeriod, - loadLastValue, - } - - if (loadData) { - await getRollupsList() - if (!selectedRollup.value) { - selectedRollup.value = rollupsList.value[0] - } - - comparisonBarWidth.value = comparisonChartEl.value.wrapper.getBoundingClientRect().width - - await getSizeSeries() - await getPfbSeries() - await getFeeSeries() - await getTVLSeries() - } - +const handleChangeChartView = () => { if (chartView.value === "line") { - buildLineChart( - sizeSeriesChartEl.value.wrapper, - loadLastValue.value ? sizeSeries.value : sizeSeries.value.slice(0, sizeSeries.value.length - 1), - () => (showSeriesTooltip.value = true), - () => (showSeriesTooltip.value = false), - "size", - tooltipConfig - ) - buildLineChart( - pfbSeriesChartEl.value.wrapper, - loadLastValue.value ? pfbSeries.value : pfbSeries.value.slice(0, pfbSeries.value.length - 1), - () => (showPfbTooltip.value = true), - () => (showPfbTooltip.value = false), - "pfb", - tooltipConfig - ) - buildLineChart( - feeSeriesChartEl.value.wrapper, - loadLastValue.value ? feeSeries.value : feeSeries.value.slice(0, feeSeries.value.length - 1), - () => (showFeeTooltip.value = true), - () => (showFeeTooltip.value = false), - "fee", - tooltipConfig - ) - buildLineChart( - tvlSeriesChartEl.value.wrapper, - loadLastValue.value ? tvlSeries.value : tvlSeries.value.slice(0, tvlSeries.value.length - 1), - () => (showTVLTooltip.value = true), - () => (showTVLTooltip.value = false), - "tvl", - tooltipConfig - ) + chartView.value = "bar" } else { - buildBarChart( - sizeSeriesChartEl.value.wrapper, - loadLastValue.value ? sizeSeries.value : sizeSeries.value.slice(0, sizeSeries.value.length - 1), - () => (showSeriesTooltip.value = true), - () => (showSeriesTooltip.value = false), - "size", - tooltipConfig, - ) - buildBarChart( - pfbSeriesChartEl.value.wrapper, - loadLastValue.value ? pfbSeries.value : pfbSeries.value.slice(0, pfbSeries.value.length - 1), - () => (showPfbTooltip.value = true), - () => (showPfbTooltip.value = false), - "pfb", - tooltipConfig, - ) - buildBarChart( - feeSeriesChartEl.value.wrapper, - loadLastValue.value ? feeSeries.value : feeSeries.value.slice(0, feeSeries.value.length - 1), - () => (showFeeTooltip.value = true), - () => (showFeeTooltip.value = false), - "fee", - tooltipConfig, - ) - buildBarChart( - tvlSeriesChartEl.value.wrapper, - loadLastValue.value ? tvlSeries.value : tvlSeries.value.slice(0, tvlSeries.value.length - 1), - () => (showTVLTooltip.value = true), - () => (showTVLTooltip.value = false), - "tvl", - tooltipConfig, - ) + chartView.value = "line" } +} - await prepareComparisonData() +const fetchAllData = async () => { + comparisonData.value[0] = {} + comparisonData.value[1] = {} - isLoading.value = false + await generateSeries(seriesConfig) + await prepareComparisonData() } watch( () => selectedPeriodIdx.value, - () => { - comparisonData.value[0] = {} - comparisonData.value[1] = {} - buildRollupCharts() - }, + () => fetchAllData(), ) watch( @@ -516,7 +329,7 @@ watch( () => { updateUserSettings() if (!isLoading.value) { - buildRollupCharts(false) + // buildRollupCharts(false) } }, ) @@ -533,34 +346,14 @@ watch( watch( () => selectedTvlDataSource.value, - async () => { - if (!isLoading.value) { - await getTVLSeries() - if (chartView.value === "line") { - buildLineChart( - tvlSeriesChartEl.value.wrapper, - loadLastValue.value ? tvlSeries.value : tvlSeries.value.slice(0, tvlSeries.value.length - 1), - () => (showTVLTooltip.value = true), - () => (showTVLTooltip.value = false), - "tvl", - ) - } else { - buildBarChart( - tvlSeriesChartEl.value.wrapper, - loadLastValue.value ? tvlSeries.value : tvlSeries.value.slice(0, tvlSeries.value.length - 1), - () => (showTVLTooltip.value = true), - () => (showTVLTooltip.value = false), - "tvl", - ) - } + async (newDataSource, oldDataSource) => { + if (oldDataSource && newDataSource?.name !== oldDataSource?.name) { + await generateSeries([seriesConfig.find((el) => el.metric === "tvl")]) } }, + { deep: true } ) -const debouncedRedraw = useDebounceFn((e) => { - buildRollupCharts() -}, 500) - onBeforeMount(() => { isLoading.value = true const settings = JSON.parse(localStorage.getItem("settings")) @@ -569,8 +362,6 @@ onBeforeMount(() => { }) onMounted(async () => { - window.addEventListener("resize", debouncedRedraw) - if (props.rollup["l2_beat"]) { tvlDataSources.value.push({ name: "l2beat", title: "L2Beat" }) } @@ -579,16 +370,13 @@ onMounted(async () => { } selectedTvlDataSource.value = tvlDataSources.value[0] - buildRollupCharts() -}) - -onBeforeUnmount(() => { - window.removeEventListener("resize", debouncedRedraw) + await fetchAllData() }) - diff --git a/components/ui/ChartOnEntityPage.vue b/components/ui/ChartOnEntityPage.vue new file mode 100644 index 00000000..c8dede96 --- /dev/null +++ b/components/ui/ChartOnEntityPage.vue @@ -0,0 +1,331 @@ + + + + + diff --git a/services/utils/charts.js b/services/utils/entityCharts.js similarity index 94% rename from services/utils/charts.js rename to services/utils/entityCharts.js index f0024877..3c2f2dd4 100644 --- a/services/utils/charts.js +++ b/services/utils/entityCharts.js @@ -58,15 +58,15 @@ export const buildLineChart = (chartEl, data, onEnter, onLeave, metric, tooltipC if (tooltipText) tooltipText.value = data[idx].value if (tooltipEl && tooltipEl.value) { - if (idx > parseInt(selectedPeriod?.value?.value / 2)) { + if (idx > parseInt(selectedPeriod?.value / 2)) { tooltipDynamicXPosition.value = tooltipXOffset.value - tooltipEl.value.wrapper.getBoundingClientRect().width - 16 } else { tooltipDynamicXPosition.value = tooltipXOffset.value + 16 } } - let tf = selectedPeriod?.value?.timeframe - if (metric === "tvl" && ["hour", "week"].includes(selectedPeriod?.value?.timeframe)) { + let tf = selectedPeriod?.timeframe + if (metric === "tvl" && ["hour", "week"].includes(selectedPeriod?.timeframe)) { tf = "day" } @@ -145,9 +145,9 @@ export const buildLineChart = (chartEl, data, onEnter, onLeave, metric, tooltipC .attr("stroke-width", 2) .attr("stroke-linecap", "round") .attr("stroke-linejoin", "round") - .attr("d", line(loadLastValue?.value ? data.slice(0, data.length - 1) : data)) + .attr("d", line(loadLastValue ? data.slice(0, data.length - 1) : data)) - if (loadLastValue?.value) { + if (loadLastValue) { // Create pattern const defs = svg.append("defs") const pattern = defs @@ -171,7 +171,7 @@ export const buildLineChart = (chartEl, data, onEnter, onLeave, metric, tooltipC } const totalDuration = 1_000 - const path1Duration = loadLastValue?.value ? (totalDuration / data.length) * (data.length - 1) : totalDuration + const path1Duration = loadLastValue? (totalDuration / data.length) * (data.length - 1) : totalDuration const path1Length = path1.node().getTotalLength() path1 @@ -182,7 +182,7 @@ export const buildLineChart = (chartEl, data, onEnter, onLeave, metric, tooltipC .ease(d3.easeLinear) .attr("stroke-dashoffset", 0) - if (loadLastValue?.value) { + if (loadLastValue) { const path2Duration = totalDuration / data.length const path2Length = path2.node().getTotalLength() + 1 @@ -283,15 +283,17 @@ export const buildBarChart = (chartEl, data, onEnter, onLeave, metric, tooltipCo if (tooltipText) tooltipText.value = data[idx].value if (tooltipEl && tooltipEl.value) { - if (idx > parseInt(selectedPeriod?.value?.value / 2)) { - tooltipDynamicXPosition.value = tooltipXOffset.value - tooltipEl.value.wrapper.getBoundingClientRect().width - 16 + const tooltipDomElement = tooltipEl.value.$el || tooltipEl.value + + if (idx > parseInt(selectedPeriod?.value / 2)) { + tooltipDynamicXPosition.value = tooltipXOffset.value - tooltipDomElement.getBoundingClientRect().width - 16 } else { tooltipDynamicXPosition.value = tooltipXOffset.value + 16 } } - let tf = selectedPeriod?.value?.timeframe - if (metric === "tvl" && ["hour", "week"].includes(selectedPeriod?.value?.timeframe)) { + let tf = selectedPeriod?.timeframe + if (metric === "tvl" && ["hour", "week"].includes(selectedPeriod?.timeframe)) { tf = "day" } @@ -364,7 +366,6 @@ export const buildBarChart = (chartEl, data, onEnter, onLeave, metric, tooltipCo if (data.length) { const { loadLastValue } = tooltipConfig - /** Chart Bars */ svg.append("defs") .append("pattern") @@ -390,7 +391,7 @@ export const buildBarChart = (chartEl, data, onEnter, onLeave, metric, tooltipCo .attr("x", (d) => x(new Date(d.date))) .attr("y", (d) => y(d.value)) .attr("width", barWidth) - .attr("fill", (d, i) => (loadLastValue?.value && i === data.length - 1 ? `url(#diagonal-stripe)` : "var(--brand)")) + .attr("fill", (d, i) => (loadLastValue && i === data.length - 1 ? `url(#diagonal-stripe)` : "var(--brand)")) .transition() .duration(1_000) .attr("height", (d) => Math.max(height - marginBottom - 6 - y(d.value), 0)) diff --git a/services/utils/index.js b/services/utils/index.js index c41cb4f1..5f226fe2 100644 --- a/services/utils/index.js +++ b/services/utils/index.js @@ -2,4 +2,4 @@ export * from "./general" export * from "./amounts" export * from "./strings" export * from "./d3" -export * from "./charts" +export * from "./entityCharts" From 4376f1e2ec6600f2a48fb0312839af39a680ce76 Mon Sep 17 00:00:00 2001 From: sstark21 Date: Fri, 22 Aug 2025 14:20:56 +0100 Subject: [PATCH 07/13] fixiki --- components/modules/address/AddressCharts.vue | 2 +- components/modules/namespace/NamespaceCharts.vue | 2 +- services/utils/entityCharts.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/modules/address/AddressCharts.vue b/components/modules/address/AddressCharts.vue index 239ea372..4e25d056 100644 --- a/components/modules/address/AddressCharts.vue +++ b/components/modules/address/AddressCharts.vue @@ -12,7 +12,7 @@ import Toggle from "@/components/ui/Toggle.vue" /** Services */ import { abbreviate, tia } from "@/services/utils" -import { buildLineChart, buildBarChart } from "@/services/utils/charts" +import { buildLineChart, buildBarChart } from "@/services/utils/entityCharts" /** API */ import { fetchAddressSeries } from "@/services/api/stats" diff --git a/components/modules/namespace/NamespaceCharts.vue b/components/modules/namespace/NamespaceCharts.vue index 3145f63a..aadca757 100644 --- a/components/modules/namespace/NamespaceCharts.vue +++ b/components/modules/namespace/NamespaceCharts.vue @@ -12,7 +12,7 @@ import Toggle from "@/components/ui/Toggle.vue" /** Services */ import { abbreviate, formatBytes } from "@/services/utils" -import { buildLineChart, buildBarChart } from "@/services/utils/charts" +import { buildLineChart, buildBarChart } from "@/services/utils/entityCharts" /** API */ import { fetchNamespaceSeries } from "@/services/api/stats" diff --git a/services/utils/entityCharts.js b/services/utils/entityCharts.js index 3c2f2dd4..11d50635 100644 --- a/services/utils/entityCharts.js +++ b/services/utils/entityCharts.js @@ -171,7 +171,7 @@ export const buildLineChart = (chartEl, data, onEnter, onLeave, metric, tooltipC } const totalDuration = 1_000 - const path1Duration = loadLastValue? (totalDuration / data.length) * (data.length - 1) : totalDuration + const path1Duration = loadLastValue ? (totalDuration / data.length) * (data.length - 1) : totalDuration const path1Length = path1.node().getTotalLength() path1 From 0bdfdf6ea5a7bf6ff2331960cbf9632b91300d23 Mon Sep 17 00:00:00 2001 From: sstark21 Date: Mon, 25 Aug 2025 18:02:59 +0100 Subject: [PATCH 08/13] mid of optimization --- components/modules/address/AddressCharts.vue | 421 +++-------------- .../modules/namespace/NamespaceCharts.vue | 441 +++--------------- components/modules/rollup/RollupCharts.vue | 236 +++++----- services/utils/entityCharts.js | 77 +++ 4 files changed, 296 insertions(+), 879 deletions(-) diff --git a/components/modules/address/AddressCharts.vue b/components/modules/address/AddressCharts.vue index 4e25d056..84ed0222 100644 --- a/components/modules/address/AddressCharts.vue +++ b/components/modules/address/AddressCharts.vue @@ -9,10 +9,14 @@ import Button from "@/components/ui/Button.vue" import { Dropdown, DropdownItem } from "@/components/ui/Dropdown" import Popover from "@/components/ui/Popover.vue" import Toggle from "@/components/ui/Toggle.vue" +import ChartOnEntityPage from "@/components/ui/ChartOnEntityPage.vue" +import Icon from "@/components/Icon.vue" +import Text from "@/components/Text.vue" +import Flex from "@/components/Flex.vue" /** Services */ import { abbreviate, tia } from "@/services/utils" -import { buildLineChart, buildBarChart } from "@/services/utils/entityCharts" +import { createDataMap, generateDateForPeriod, generateSeriesData, PERIODS as periods } from "@/services/utils/entityCharts" /** API */ import { fetchAddressSeries } from "@/services/api/stats" @@ -30,29 +34,7 @@ const props = defineProps({ /** Chart settings */ const selectedPeriodIdx = ref(2) -const periods = ref([ - { - title: "Last 24 hours", - value: 24, - timeframe: "hour", - }, - { - title: "Last 7 days", - value: 7, - timeframe: "day", - }, - { - title: "Last 31 days", - value: 30, - timeframe: "day", - }, - { - title: "Last 12 months", - value: 12, - timeframe: "month", - }, -]) -const selectedPeriod = computed(() => periods.value[selectedPeriodIdx.value]) +const selectedPeriod = computed(() => periods[selectedPeriodIdx.value]) const chartView = ref("line") const loadLastValue = ref(true) @@ -64,14 +46,6 @@ const handleClose = () => { isOpen.value = false } -const handleChangeChartView = () => { - if (chartView.value === "line") { - chartView.value = "bar" - } else { - chartView.value = "line" - } -} - const updateUserSettings = () => { settingsStore.chart = { ...settingsStore.chart, @@ -80,29 +54,16 @@ const updateUserSettings = () => { } } -/** Charts */ -const chartWrapperEl = useTemplateRef("chartWrapperEl") -const txSeriesChartEl = useTemplateRef("txSeriesChartEl") -const feeSeriesChartEl = useTemplateRef("feeSeriesChartEl") - /** Data */ const isLoading = ref(false) const txSeries = ref([]) const feeSeries = ref([]) -/** Tooltip */ -const showTxTooltip = ref(false) -const showFeeTooltip = ref(false) -const tooltipEl = ref() -const tooltipXOffset = ref(0) -const tooltipYOffset = ref(0) -const tooltipYDataOffset = ref(0) -const tooltipDynamicXPosition = ref(0) -const tooltipText = ref("") - -const badgeEl = ref() -const badgeText = ref("") -const badgeOffset = ref(0) +/** Series config */ +const seriesConfig = [ + { name: "tx_count", metric: "tx", series: txSeries }, + { name: "fee", metric: "fee", series: feeSeries }, +] const fetchData = async (metric) => { const data = await fetchAddressSeries({ @@ -120,137 +81,46 @@ const fetchData = async (metric) => { return data } -const getTxSeries = async () => { - txSeries.value = [] - const txSeriesRawData = await fetchData("tx_count") - const txSeriesMap = {} - txSeriesRawData.forEach((item) => { - txSeriesMap[ - DateTime.fromISO(item.time).toFormat(["day", "month"].includes(selectedPeriod.value.timeframe) ? "y-LL-dd" : "y-LL-dd-HH") - ] = item.value - }) - - for (let i = 1; i < selectedPeriod.value.value + 1; i++) { - let dt - if (selectedPeriod.value.timeframe === "month") { - dt = DateTime.now() - .startOf("month") - .minus({ - months: selectedPeriod.value.timeframe === "month" ? selectedPeriod.value.value - i : 0, - }) - } else { - dt = DateTime.now().minus({ - days: selectedPeriod.value.timeframe === "day" ? selectedPeriod.value.value - i : 0, - hours: selectedPeriod.value.timeframe === "hour" ? selectedPeriod.value.value - i : 0, - }) - } - txSeries.value.push({ - date: dt.toJSDate(), - value: - parseInt(txSeriesMap[dt.toFormat(["day", "month"].includes(selectedPeriod.value.timeframe) ? "y-LL-dd" : "y-LL-dd-HH")]) || - 0, - }) - } -} - -const getFeeSeries = async () => { - feeSeries.value = [] - - const feeSeriesRawData = await fetchData("fee") +const generateSeries = async (configs) => { + isLoading.value = true - const feeSeriesMap = {} - feeSeriesRawData.forEach((item) => { - feeSeriesMap[ - DateTime.fromISO(item.time).toFormat(["day", "month"].includes(selectedPeriod.value.timeframe) ? "y-LL-dd" : "y-LL-dd-HH") - ] = item.value - }) + await Promise.all( + configs.map(async (config) => { + const rawData = await fetchData(config.name) + const dataMap = createDataMap(rawData, selectedPeriod.value.timeframe) + generateSeriesData(selectedPeriod.value, dataMap, config.series) + }), + ) - for (let i = 1; i < selectedPeriod.value.value + 1; i++) { - let dt - if (selectedPeriod.value.timeframe === "month") { - dt = DateTime.now() - .startOf("month") - .minus({ - months: selectedPeriod.value.timeframe === "month" ? selectedPeriod.value.value - i : 0, - }) - } else { - dt = DateTime.now().minus({ - days: selectedPeriod.value.timeframe === "day" ? selectedPeriod.value.value - i : 0, - hours: selectedPeriod.value.timeframe === "hour" ? selectedPeriod.value.value - i : 0, - }) - } - feeSeries.value.push({ - date: dt.toJSDate(), - value: - parseInt(feeSeriesMap[dt.toFormat(["day", "month"].includes(selectedPeriod.value.timeframe) ? "y-LL-dd" : "y-LL-dd-HH")]) || - 0, - }) - } + isLoading.value = false } -const buildAddressCharts = async (loadData = true) => { - isLoading.value = true - if (loadData) { - await getTxSeries() - await getFeeSeries() - } - +const handleChangeChartView = () => { if (chartView.value === "line") { - buildLineChart( - txSeriesChartEl.value.wrapper, - loadLastValue.value ? txSeries.value : txSeries.value.slice(0, txSeries.value.length - 1), - () => (showTxTooltip.value = true), - () => (showTxTooltip.value = false), - ) - buildLineChart( - feeSeriesChartEl.value.wrapper, - loadLastValue.value ? feeSeries.value : feeSeries.value.slice(0, feeSeries.value.length - 1), - () => (showFeeTooltip.value = true), - () => (showFeeTooltip.value = false), - ) + chartView.value = "bar" } else { - buildBarChart( - txSeriesChartEl.value.wrapper, - loadLastValue.value ? txSeries.value : txSeries.value.slice(0, txSeries.value.length - 1), - () => (showTxTooltip.value = true), - () => (showTxTooltip.value = false), - "tx_count", - ) - buildBarChart( - feeSeriesChartEl.value.wrapper, - loadLastValue.value ? feeSeries.value : feeSeries.value.slice(0, feeSeries.value.length - 1), - () => (showFeeTooltip.value = true), - () => (showFeeTooltip.value = false), - "fee", - ) + chartView.value = "line" } +} - isLoading.value = false +const fetchAllData = async () => { + await generateSeries(seriesConfig) } watch( () => selectedPeriodIdx.value, - () => { - buildAddressCharts() - }, + () => fetchAllData(), ) watch( () => [chartView.value, loadLastValue.value], () => { updateUserSettings() - if (!isLoading.value) { - buildAddressCharts(false) - } }, ) -const debouncedRedraw = useDebounceFn((e) => { - buildAddressCharts() -}, 500) - onBeforeMount(() => { isLoading.value = true const settings = JSON.parse(localStorage.getItem("settings")) @@ -259,13 +129,7 @@ onBeforeMount(() => { }) onMounted(async () => { - window.addEventListener("resize", debouncedRedraw) - - buildAddressCharts() -}) - -onBeforeUnmount(() => { - window.removeEventListener("resize", debouncedRedraw) + await fetchAllData() }) @@ -341,207 +205,32 @@ onBeforeUnmount(() => { - - Txs - - - - - {{ abbreviate(Math.max(...txSeries.map((d) => d.value)), 0) }} - - - - - {{ abbreviate(Math.round(Math.max(...txSeries.map((d) => d.value)) / 2), 0) }} - - - - 0 - - - - - - - {{ - DateTime.now() - .minus({ days: selectedPeriod.value - 1 }) - .toFormat("LLL dd") - }} - - - {{ DateTime.now().minus({ hours: selectedPeriod.value }).set({ minutes: 0 }).toFormat("hh:mm a") }} - - - {{ - selectedPeriod.timeframe === "day" ? "Today" : "Now" - }} - - - - -
-
-
-
- - {{ badgeText }} - -
- - - Txs - {{ tooltipText }} - - -
- - - - - - - - Fee Spent - - - - - {{ - tia(Math.max(...feeSeries.map((d) => d.value)), 0) > 1 - ? tia(Math.max(...feeSeries.map((d) => d.value)), 0) - : tia(Math.max(...feeSeries.map((d) => d.value)), 2) - }} - TIA - - - - - {{ - tia(Math.round(Math.max(...feeSeries.map((d) => d.value)) / 2), 0) > 1 - ? tia(Math.round(Math.max(...feeSeries.map((d) => d.value)) / 2), 0) - : tia(Math.round(Math.max(...feeSeries.map((d) => d.value)) / 2), 2) - }} - TIA - - - - 0 - - - - - - - {{ - DateTime.now() - .minus({ days: selectedPeriod.value - 1 }) - .toFormat("LLL dd") - }} - - - {{ DateTime.now().minus({ hours: selectedPeriod.value }).set({ minutes: 0 }).toFormat("hh:mm a") }} - - - {{ - selectedPeriod.timeframe === "day" ? "Today" : "Now" - }} - - - - -
-
-
-
- - {{ badgeText }} - -
- - - Spent - {{ tia(tooltipText) }} TIA - - -
- - - - - + + + diff --git a/components/modules/namespace/NamespaceCharts.vue b/components/modules/namespace/NamespaceCharts.vue index aadca757..cf7d5272 100644 --- a/components/modules/namespace/NamespaceCharts.vue +++ b/components/modules/namespace/NamespaceCharts.vue @@ -9,10 +9,14 @@ import Button from "@/components/ui/Button.vue" import { Dropdown, DropdownItem } from "@/components/ui/Dropdown" import Popover from "@/components/ui/Popover.vue" import Toggle from "@/components/ui/Toggle.vue" +import ChartOnEntityPage from "@/components/ui/ChartOnEntityPage.vue" +import Icon from "@/components/Icon.vue" +import Text from "@/components/Text.vue" +import Flex from "@/components/Flex.vue" /** Services */ import { abbreviate, formatBytes } from "@/services/utils" -import { buildLineChart, buildBarChart } from "@/services/utils/entityCharts" +import { createDataMap, generateDateForPeriod, generateSeriesData, PERIODS as periods } from "@/services/utils/entityCharts" /** API */ import { fetchNamespaceSeries } from "@/services/api/stats" @@ -30,29 +34,7 @@ const props = defineProps({ /** Chart settings */ const selectedPeriodIdx = ref(2) -const periods = ref([ - { - title: "Last 24 hours", - value: 24, - timeframe: "hour", - }, - { - title: "Last 7 days", - value: 7, - timeframe: "day", - }, - { - title: "Last 31 days", - value: 30, - timeframe: "day", - }, - { - title: "Last 12 months", - value: 12, - timeframe: "month", - }, -]) -const selectedPeriod = computed(() => periods.value[selectedPeriodIdx.value]) +const selectedPeriod = computed(() => periods[selectedPeriodIdx.value]) const chartView = ref("line") const loadLastValue = ref(true) @@ -64,14 +46,6 @@ const handleClose = () => { isOpen.value = false } -const handleChangeChartView = () => { - if (chartView.value === "line") { - chartView.value = "bar" - } else { - chartView.value = "line" - } -} - const updateUserSettings = () => { settingsStore.chart = { ...settingsStore.chart, @@ -80,69 +54,23 @@ const updateUserSettings = () => { } } -/** Charts */ -const chartWrapperEl = ref() -const sizeSeriesChartEl = ref() -const pfbSeriesChartEl = ref() - /** Data */ const isLoading = ref(false) const sizeSeries = ref([]) const pfbSeries = ref([]) -/** Tooltip */ -const showSeriesTooltip = ref(false) -const showPfbTooltip = ref(false) -const tooltipEl = ref() -const tooltipXOffset = ref(0) -const tooltipYOffset = ref(0) -const tooltipYDataOffset = ref(0) -const tooltipDynamicXPosition = ref(0) -const tooltipText = ref("") - -const badgeEl = ref() -const badgeText = ref("") -const badgeOffset = ref(0) - -const xAxisLabels = computed(() => { - let labels = { - firstDate: "", - lastDate: "", - } - - switch (selectedPeriod.value.timeframe) { - case "month": - labels.firstDate = DateTime.now() - .minus({ months: selectedPeriod.value.value - 1 }) - .toFormat("LLL y") - labels.lastDate = loadLastValue.value ? DateTime.now().toFormat("LLL") : DateTime.now().minus({ months: 1 }).toFormat("LLL") - break - case "day": - labels.firstDate = DateTime.now() - .minus({ days: selectedPeriod.value.value - 1 }) - .toFormat("LLL dd") - labels.lastDate = loadLastValue.value ? "Today" : DateTime.now().minus({ days: 1 }).toFormat("LLL dd") - break - default: - labels.firstDate = DateTime.now() - .minus({ hours: selectedPeriod.value.value - 1 }) - .set({ minutes: 0 }) - .toFormat("hh:mm a") - labels.lastDate = loadLastValue.value ? "Now" : DateTime.now().minus({ hours: 1 }).set({ minutes: 0 }).toFormat("hh:mm a") - break - } - - return labels -}) +/** Series config */ +const seriesConfig = [ + { name: "size", metric: "size", series: sizeSeries }, + { name: "pfb_count", metric: "pfb", series: pfbSeries }, +] -const fetchData = async (metric, from) => { - const data = await fetchNamespaceSeries({ +const fetchData = async (metric) => { + return await fetchNamespaceSeries({ id: props.id, name: metric, timeframe: selectedPeriod.value.timeframe, - from: from - ? from - : parseInt( + from: parseInt( DateTime.now().minus({ days: selectedPeriod.value.timeframe === "day" ? selectedPeriod.value.value : 0, hours: selectedPeriod.value.timeframe === "hour" ? selectedPeriod.value.value : 0, @@ -150,145 +78,47 @@ const fetchData = async (metric, from) => { }).ts / 1_000, ), }) - - return data -} - -const getSizeSeries = async () => { - sizeSeries.value = [] - - const sizeSeriesRawData = await fetchData("size") - - const sizeSeriesMap = {} - sizeSeriesRawData.forEach((item) => { - sizeSeriesMap[ - DateTime.fromISO(item.time).toFormat(["day", "month"].includes(selectedPeriod.value.timeframe) ? "y-LL-dd" : "y-LL-dd-HH") - ] = item.value - }) - - for (let i = 1; i < selectedPeriod.value.value + 1; i++) { - let dt - if (selectedPeriod.value.timeframe === "month") { - dt = DateTime.now() - .startOf("month") - .minus({ - months: selectedPeriod.value.timeframe === "month" ? selectedPeriod.value.value - i : 0, - }) - } else { - dt = DateTime.now().minus({ - days: selectedPeriod.value.timeframe === "day" ? selectedPeriod.value.value - i : 0, - hours: selectedPeriod.value.timeframe === "hour" ? selectedPeriod.value.value - i : 0, - }) - } - sizeSeries.value.push({ - date: dt.toJSDate(), - value: - parseInt( - sizeSeriesMap[dt.toFormat(["day", "month"].includes(selectedPeriod.value.timeframe) ? "y-LL-dd" : "y-LL-dd-HH")], - ) || 0, - }) - } } -const getPfbSeries = async () => { - pfbSeries.value = [] - const pfbSeriesRawData = await fetchData("pfb_count") +const generateSeries = async (configs) => { + isLoading.value = true - const pfbSeriesMap = {} - pfbSeriesRawData.forEach((item) => { - pfbSeriesMap[ - DateTime.fromISO(item.time).toFormat(["day", "month"].includes(selectedPeriod.value.timeframe) ? "y-LL-dd" : "y-LL-dd-HH") - ] = item.value - }) + await Promise.all( + configs.map(async (config) => { + const rawData = await fetchData(config.name) + const dataMap = createDataMap(rawData, selectedPeriod.value.timeframe) + generateSeriesData(selectedPeriod.value, dataMap, config.series) + }), + ) - for (let i = 1; i < selectedPeriod.value.value + 1; i++) { - let dt - if (selectedPeriod.value.timeframe === "month") { - dt = DateTime.now() - .startOf("month") - .minus({ - months: selectedPeriod.value.timeframe === "month" ? selectedPeriod.value.value - i : 0, - }) - } else { - dt = DateTime.now().minus({ - days: selectedPeriod.value.timeframe === "day" ? selectedPeriod.value.value - i : 0, - hours: selectedPeriod.value.timeframe === "hour" ? selectedPeriod.value.value - i : 0, - }) - } - pfbSeries.value.push({ - date: dt.toJSDate(), - value: - parseInt(pfbSeriesMap[dt.toFormat(["day", "month"].includes(selectedPeriod.value.timeframe) ? "y-LL-dd" : "y-LL-dd-HH")]) || - 0, - }) - } + isLoading.value = false } -const buildNamespaceCharts = async (loadData = true) => { - isLoading.value = true - - if (loadData) { - await getSizeSeries() - await getPfbSeries() - } - +const handleChangeChartView = () => { if (chartView.value === "line") { - buildLineChart( - sizeSeriesChartEl.value.wrapper, - loadLastValue.value ? sizeSeries.value : sizeSeries.value.slice(0, sizeSeries.value.length - 1), - () => (showSeriesTooltip.value = true), - () => (showSeriesTooltip.value = false), - "size", - ) - buildLineChart( - pfbSeriesChartEl.value.wrapper, - loadLastValue.value ? pfbSeries.value : pfbSeries.value.slice(0, pfbSeries.value.length - 1), - () => (showPfbTooltip.value = true), - () => (showPfbTooltip.value = false), - "pfb", - ) + chartView.value = "bar" } else { - buildBarChart( - sizeSeriesChartEl.value.wrapper, - loadLastValue.value ? sizeSeries.value : sizeSeries.value.slice(0, sizeSeries.value.length - 1), - () => (showSeriesTooltip.value = true), - () => (showSeriesTooltip.value = false), - "size", - ) - buildBarChart( - pfbSeriesChartEl.value.wrapper, - loadLastValue.value ? pfbSeries.value : pfbSeries.value.slice(0, pfbSeries.value.length - 1), - () => (showPfbTooltip.value = true), - () => (showPfbTooltip.value = false), - "pfb", - ) + chartView.value = "line" } +} - isLoading.value = false +const fetchAllData = async () => { + await generateSeries(seriesConfig) } watch( () => selectedPeriodIdx.value, - () => { - buildNamespaceCharts() - }, + () => fetchAllData(), ) watch( () => [chartView.value, loadLastValue.value], () => { updateUserSettings() - if (!isLoading.value) { - buildNamespaceCharts(false) - } }, ) -const debouncedRedraw = useDebounceFn((e) => { - buildNamespaceCharts() -}, 500) - onBeforeMount(() => { isLoading.value = true const settings = JSON.parse(localStorage.getItem("settings")) @@ -297,13 +127,7 @@ onBeforeMount(() => { }) onMounted(async () => { - window.addEventListener("resize", debouncedRedraw) - - buildNamespaceCharts() -}) - -onBeforeUnmount(() => { - window.removeEventListener("resize", debouncedRedraw) + await fetchAllData() }) @@ -378,182 +202,31 @@ onBeforeUnmount(() => { - - DA Usage - - - - - {{ formatBytes(Math.max(...sizeSeries.map((d) => d.value)), 0) }} - - - - - {{ formatBytes(Math.round(Math.max(...sizeSeries.map((d) => d.value)) / 2), 0) }} - - - - 0 - - - - - - - {{ xAxisLabels.firstDate }} - - - - {{ xAxisLabels.lastDate }} - - - - - -
-
-
-
- - {{ badgeText }} - -
- - - Usage - {{ formatBytes(tooltipText) }} - - -
- - - - - - - - Pay For Blobs Count - - - - - {{ abbreviate(Math.max(...pfbSeries.map((d) => d.value)), 0) }} - - - - - {{ abbreviate(Math.round(Math.max(...pfbSeries.map((d) => d.value)) / 2), 0) }} - - - - 0 - - - - - - - {{ xAxisLabels.firstDate }} - - - - {{ xAxisLabels.lastDate }} - - - - - -
-
-
-
- - {{ badgeText }} - -
- - - Count - {{ abbreviate(tooltipText) }} - - -
- - - - - + + + diff --git a/components/modules/rollup/RollupCharts.vue b/components/modules/rollup/RollupCharts.vue index 69d087db..1173a3ab 100644 --- a/components/modules/rollup/RollupCharts.vue +++ b/components/modules/rollup/RollupCharts.vue @@ -18,7 +18,7 @@ import Flex from "@/components/Flex.vue" /** Services */ import { abbreviate, formatBytes, sortArrayOfObjects, spaces, tia } from "@/services/utils" -import { buildLineChart, buildBarChart } from "@/services/utils/entityCharts" +import { getFormatKey, createDataMap, generateDateForPeriod, generateSeriesData, PERIODS as periods } from "@/services/utils/entityCharts" /** API */ import { fetchRollupSeries } from "@/services/api/stats" @@ -37,31 +37,8 @@ const props = defineProps({ /** Chart settings */ const selectedPeriodIdx = ref(2) -const periods = ref([ - { - title: "Last 24 hours", - value: 24, - timeframe: "hour", - }, - { - title: "Last 7 days", - value: 7, - timeframe: "day", - }, - { - title: "Last 31 days", - value: 30, - timeframe: "day", - }, - { - title: "Last 12 months", - value: 12, - timeframe: "month", - }, -]) - -const selectedPeriod = computed(() => periods.value[selectedPeriodIdx.value]) +const selectedPeriod = computed(() => periods[selectedPeriodIdx.value]) const chartView = ref("line") const loadLastValue = ref(true) @@ -97,12 +74,47 @@ const rollupsList = ref() const comparisonData = ref([]) const selectedRollup = ref() -/** Series config */ const seriesConfig = [ - { name: "size", metric: "size", series: sizeSeries }, - { name: "blobs_count", metric: "pfb", series: pfbSeries }, - { name: "fee", metric: "fee", series: feeSeries }, - { name: "tvl", metric: "tvl", series: tvlSeries }, + { + name: "size", + metric: "size", + series: sizeSeries, + title: "DA Usage", + tooltipLabel: "Usage", + yAxisFormatter: formatBytes, + tooltipValueFormatter: formatBytes, + unit: null, + }, + { + name: "blobs_count", + metric: "pfb", + series: pfbSeries, + title: "Blobs Count", + tooltipLabel: "Count", + yAxisFormatter: abbreviate, + tooltipValueFormatter: abbreviate, + unit: null, + }, + { + name: "fee", + metric: "fee", + series: feeSeries, + title: "Fee spent", + tooltipLabel: "Spent", + yAxisFormatter: tia, + tooltipValueFormatter: tia, + unit: "TIA", + }, + { + name: "tvl", + metric: "tvl", + series: tvlSeries, + title: "TVL", + tooltipLabel: "TVL", + yAxisFormatter: abbreviate, + tooltipValueFormatter: abbreviate, + unit: "$", + }, ] const getRollupsList = async () => { @@ -113,82 +125,62 @@ const getRollupsList = async () => { rollupsList.value = sortArrayOfObjects(data, "slug").filter((r) => r.id !== props.rollup.id) } -const fetchData = async (rollup, metric, from, timeframe) => { - if (metric === "tvl") { - let from = "" - - const { timeframe: tf, value: periodValue } = selectedPeriod.value - - if (["hour", "week"].includes(tf)) { - from = parseInt(DateTime.now().minus({ days: 30 }).ts / 1_000) - tf = "day" - periodValue = 30 - } +const fetchTVLData = async () => { + let from = "" + let tf = selectedPeriod.value.timeframe + let periodValue = selectedPeriod.value.value - return await fetchRollupTVL({ - dataSource: selectedTvlDataSource.value?.name, - slug: props.rollup.slug, - period: tf, - from, - }) - } else { - return await fetchRollupSeries({ - id: rollup.id, - name: metric, - timeframe: timeframe ? timeframe : selectedPeriod.value.timeframe, - from: from - ? from - : parseInt( - DateTime.now().minus({ - days: selectedPeriod.value.timeframe === "day" ? selectedPeriod.value.value : 0, - hours: selectedPeriod.value.timeframe === "hour" ? selectedPeriod.value.value : 0, - months: selectedPeriod.value.timeframe === "month" ? selectedPeriod.value.value : 0, - }).ts / 1_000, - ), - }) + if (["hour", "week"].includes(tf)) { + from = parseInt(DateTime.now().minus({ days: 30 }).ts / 1_000) + tf = "day" + periodValue = 30 } -} - -const generateDateForPeriod = (period, index) => { - const { timeframe, value } = period - if (timeframe === "month") { - return DateTime.now() - .startOf("month") - .minus({ months: value - index }) - } else { - return DateTime.now().minus({ - [timeframe === "day" ? "days" : "hours"]: value - index, - }) - } + return await fetchRollupTVL({ + dataSource: selectedTvlDataSource.value?.name, + slug: props.rollup.slug, + period: tf, + from, + }) } -const getFormatKey = (timeframe) => { - return ["day", "month"].includes(timeframe) ? "y-LL-dd" : "y-LL-dd-HH" +const fetchData = async (rollup, metric) => { + return await fetchRollupSeries({ + id: rollup.id, + name: metric, + timeframe: selectedPeriod.value.timeframe, + from: parseInt( + DateTime.now().minus({ + days: selectedPeriod.value.timeframe === "day" ? selectedPeriod.value.value : 0, + hours: selectedPeriod.value.timeframe === "hour" ? selectedPeriod.value.value : 0, + months: selectedPeriod.value.timeframe === "month" ? selectedPeriod.value.value : 0, + }).ts / 1_000, + ), + }) } -const generateSeriesData = (period, dataMap, series) => { - series.value = [] +const generateSeries = async (configs) => { + isLoading.value = true - for (let i = 1; i < period.value + 1; i++) { - const dt = generateDateForPeriod(period, i) - const formatKey = getFormatKey(period.timeframe) - const key = dt.toFormat(formatKey) + await Promise.all( + configs.map(async (config) => { + const rawData = await fetchData(props.rollup, config.name) + const dataMap = createDataMap(rawData, selectedPeriod.value.timeframe) - series.value.push({ - date: dt.toJSDate(), - value: parseInt(dataMap[key]) || 0, - }) - } + generateSeriesData(selectedPeriod.value, dataMap, config.series) + }), + ) + + isLoading.value = false } -const generateTVLSeriesData = (period, dataMap, series, tvl = false) => { +const generateTVLSeriesData = (period, dataMap, series) => { series.value = [] let tf = period.timeframe let periodValue = period.value - if (tvl && ["hour", "week"].includes(period.timeframe)) { + if (["hour", "week"].includes(period.timeframe)) { tf = "day" periodValue = 30 } @@ -205,57 +197,42 @@ const generateTVLSeriesData = (period, dataMap, series, tvl = false) => { } } -const createDataMap = (rawData, timeframe) => { - const dataMap = {} - const formatKey = getFormatKey(timeframe) - - rawData.forEach((item) => { - dataMap[DateTime.fromISO(item.time).toFormat(formatKey)] = item.value - }) - - return dataMap -} - -const generateSeries = async (configs) => { +const generateTVLSeries = async (configs) => { isLoading.value = true - /** Get comparison chart width */ - comparisonBarWidth.value = comparisonChartEl.value.wrapper.getBoundingClientRect().width - await getRollupsList() - if (!selectedRollup.value) { - selectedRollup.value = rollupsList.value[0] - } - - await Promise.all( configs.map(async (config) => { - const rawData = await fetchData(props.rollup, config.name) + const rawData = await fetchTVLData() const dataMap = createDataMap(rawData, selectedPeriod.value.timeframe) - if (config.metric === "tvl") { - generateTVLSeriesData(selectedPeriod.value, dataMap, config.series, true) - } else { - generateSeriesData(selectedPeriod.value, dataMap, config.series) - } - - isLoading.value = false + generateTVLSeriesData(selectedPeriod.value, dataMap, config.series) }), ) + + isLoading.value = false } const isTvlDataSourcePopoverOpen = ref(false) + const handleTvlDataSourcePopoverClose = () => { isTvlDataSourcePopoverOpen.value = false } + const handleSelectTvlDataSource = (ds) => { selectedTvlDataSource.value = ds isTvlDataSourcePopoverOpen.value = false } -const prepareComparisonData = async () => { +const prepareComparisonData = async (fetchFunction) => { isLoading.value = true comparisonData.value[1] = {} + comparisonBarWidth.value = comparisonChartEl.value.wrapper.getBoundingClientRect().width + await getRollupsList() + if (!selectedRollup.value) { + selectedRollup.value = rollupsList.value[0] + } + if (!comparisonData.value[0]?.fee) { comparisonData.value[0] = { fee: feeSeries.value.reduce((sum, el) => sum + el.value, 0), @@ -315,8 +292,12 @@ const fetchAllData = async () => { comparisonData.value[0] = {} comparisonData.value[1] = {} - await generateSeries(seriesConfig) - await prepareComparisonData() + await generateSeries( + seriesConfig.filter((el) => el.metric !== "tvl"), + fetchRollupSeries, + ) + await generateTVLSeries(seriesConfig.filter((el) => el.metric === "tvl")) + await prepareComparisonData(fetchRollupSeries) } watch( @@ -328,9 +309,6 @@ watch( () => [chartView.value, loadLastValue.value], () => { updateUserSettings() - if (!isLoading.value) { - // buildRollupCharts(false) - } }, ) @@ -339,7 +317,7 @@ watch( () => { if (!isLoading.value) { comparisonData.value[1] = {} - prepareComparisonData() + prepareComparisonData(fetchRollupSeries) } }, ) @@ -348,10 +326,10 @@ watch( () => selectedTvlDataSource.value, async (newDataSource, oldDataSource) => { if (oldDataSource && newDataSource?.name !== oldDataSource?.name) { - await generateSeries([seriesConfig.find((el) => el.metric === "tvl")]) + await generateTVLSeries([seriesConfig.find((el) => el.metric === "tvl")]) } }, - { deep: true } + { deep: true }, ) onBeforeMount(() => { @@ -450,13 +428,13 @@ onMounted(async () => { diff --git a/services/utils/entityCharts.js b/services/utils/entityCharts.js index 11d50635..64c380e3 100644 --- a/services/utils/entityCharts.js +++ b/services/utils/entityCharts.js @@ -1,6 +1,83 @@ import * as d3 from "d3" import { DateTime } from "luxon" +/** + * Periods for the charts + * @type {Array} + */ +export const PERIODS = [ + { + title: "Last 24 hours", + value: 24, + timeframe: "hour", + }, + { + title: "Last 7 days", + value: 7, + timeframe: "day", + }, + { + title: "Last 31 days", + value: 30, + timeframe: "day", + }, + { + title: "Last 12 months", + value: 12, + timeframe: "month", + }, +] + +export const generateDateForPeriod = (period, index) => { + const { timeframe, value } = period + + if (timeframe === "month") { + return DateTime.now() + .startOf("month") + .minus({ months: value - index }) + } else { + return DateTime.now().minus({ + [timeframe === "day" ? "days" : "hours"]: value - index, + }) + } +} + +/** + * Get the format key for the timeframe + * @param {string} timeframe - The timeframe + * @returns {string} - The format key + */ +export const getFormatKey = (timeframe) => { + return ["day", "month"].includes(timeframe) ? "y-LL-dd" : "y-LL-dd-HH" +} + + +export const createDataMap = (rawData, timeframe) => { + const dataMap = {} + const formatKey = getFormatKey(timeframe) + + rawData.forEach((item) => { + dataMap[DateTime.fromISO(item.time).toFormat(formatKey)] = item.value + }) + + return dataMap +} + +export const generateSeriesData = (period, dataMap, series) => { + series.value = [] + + for (let i = 1; i < period.value + 1; i++) { + const dt = generateDateForPeriod(period, i) + const formatKey = getFormatKey(period.timeframe) + const key = dt.toFormat(formatKey) + + series.value.push({ + date: dt.toJSDate(), + value: parseInt(dataMap[key]) || 0, + }) + } +} + /** * Builds a line chart using D3.js * @param {HTMLElement} chartEl - DOM element for chart placement From e973ad9d008df50c0913e9fab70f2a77c7aeb7ef Mon Sep 17 00:00:00 2001 From: sstark21 Date: Tue, 26 Aug 2025 09:56:11 +0100 Subject: [PATCH 09/13] prepare rollups --- components/modules/rollup/RollupCharts.vue | 55 ++++++++-------------- components/ui/ChartOnEntityPage.vue | 51 ++++++++++++-------- services/utils/amounts.js | 7 ++- 3 files changed, 57 insertions(+), 56 deletions(-) diff --git a/components/modules/rollup/RollupCharts.vue b/components/modules/rollup/RollupCharts.vue index 1173a3ab..168b818b 100644 --- a/components/modules/rollup/RollupCharts.vue +++ b/components/modules/rollup/RollupCharts.vue @@ -17,7 +17,7 @@ import Text from "@/components/Text.vue" import Flex from "@/components/Flex.vue" /** Services */ -import { abbreviate, formatBytes, sortArrayOfObjects, spaces, tia } from "@/services/utils" +import { abbreviate, formatBytes, sortArrayOfObjects, spaces, aTia, tia } from "@/services/utils" import { getFormatKey, createDataMap, generateDateForPeriod, generateSeriesData, PERIODS as periods } from "@/services/utils/entityCharts" /** API */ @@ -81,7 +81,7 @@ const seriesConfig = [ series: sizeSeries, title: "DA Usage", tooltipLabel: "Usage", - yAxisFormatter: formatBytes, + yAxisFormatter: (value) => formatBytes(value, 0), tooltipValueFormatter: formatBytes, unit: null, }, @@ -91,7 +91,7 @@ const seriesConfig = [ series: pfbSeries, title: "Blobs Count", tooltipLabel: "Count", - yAxisFormatter: abbreviate, + yAxisFormatter: (value) => abbreviate(value, 0), tooltipValueFormatter: abbreviate, unit: null, }, @@ -101,9 +101,9 @@ const seriesConfig = [ series: feeSeries, title: "Fee spent", tooltipLabel: "Spent", - yAxisFormatter: tia, - tooltipValueFormatter: tia, - unit: "TIA", + yAxisFormatter: (value) => aTia(value, 0), + tooltipValueFormatter: aTia, + unit: null, }, { name: "tvl", @@ -117,6 +117,14 @@ const seriesConfig = [ }, ] +const sizeConfig = computed(() => seriesConfig.find((config) => config.metric === "size")) + +const pfbConfig = computed(() => seriesConfig.find((config) => config.metric === "pfb")) + +const feeConfig = computed(() => seriesConfig.find((config) => config.metric === "fee")) + +const tvlConfig = computed(() => seriesConfig.find((config) => config.metric === "tvl")) + const getRollupsList = async () => { const data = await fetchRollups({ limit: 30, @@ -292,10 +300,7 @@ const fetchAllData = async () => { comparisonData.value[0] = {} comparisonData.value[1] = {} - await generateSeries( - seriesConfig.filter((el) => el.metric !== "tvl"), - fetchRollupSeries, - ) + await generateSeries(seriesConfig.filter((el) => el.metric !== "tvl")) await generateTVLSeries(seriesConfig.filter((el) => el.metric === "tvl")) await prepareComparisonData(fetchRollupSeries) } @@ -427,58 +432,36 @@ onMounted(async () => {