diff --git a/assets/styles/base.scss b/assets/styles/base.scss
index b2d9a8b4..1a991ac8 100644
--- a/assets/styles/base.scss
+++ b/assets/styles/base.scss
@@ -78,7 +78,7 @@ $grayscale: (
--block-progress-fill-background: #33a853;
--logo-name: var(--txt-primary);
--bar-fill: rgb(243, 147, 45);
- --validator-active: #85f891;
+ --validator-active: #18d2a5;
--validator-inactive: #1ca7ed;
--validator-jailed: #f8774a;
--supply: #1ca7ed;
diff --git a/components/modules/address/AddressCharts.vue b/components/modules/address/AddressCharts.vue
index 233053bb..c1c102e5 100644
--- a/components/modules/address/AddressCharts.vue
+++ b/components/modules/address/AddressCharts.vue
@@ -1,17 +1,20 @@
@@ -659,207 +222,23 @@ 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 56d838bd..ea8cd7e8 100644
--- a/components/modules/namespace/NamespaceCharts.vue
+++ b/components/modules/namespace/NamespaceCharts.vue
@@ -9,9 +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/shared/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 { createDataMap, generateDateForPeriod, generateSeriesData, PERIODS as periods } from "@/services/utils/entityCharts"
/** API */
import { fetchNamespaceSeries } from "@/services/api/stats"
@@ -29,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)
@@ -63,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,
@@ -79,523 +54,91 @@ 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
-})
-
-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
- 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 = ""
- }
+/** Series config */
+const seriesConfig = [
+ {
+ name: "size",
+ metric: "size",
+ series: sizeSeries,
+ title: "DA Usage",
+ tooltipLabel: "Usage",
+ yAxisFormatter: (value) => formatBytes(value, 0),
+ tooltipValueFormatter: formatBytes,
+ unit: null,
+ },
+ {
+ name: "pfb_count",
+ metric: "pfb",
+ series: pfbSeries,
+ title: "Pay For Blobs Count",
+ tooltipLabel: "Count",
+ yAxisFormatter: abbreviate,
+ tooltipValueFormatter: abbreviate,
+ unit: null,
+ },
+]
- /** 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 sizeConfig = computed(() => seriesConfig.find((config) => config.metric === "size"))
+const pfbConfig = computed(() => seriesConfig.find((config) => config.metric === "pfb"))
-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(
- 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,
- ),
+ 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,
+ ),
})
-
- 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),
- )
- buildLineChart(
- pfbSeriesChartEl.value.wrapper,
- loadLastValue.value ? pfbSeries.value : pfbSeries.value.slice(0, pfbSeries.value.length - 1),
- () => (showPfbTooltip.value = true),
- () => (showPfbTooltip.value = false),
- )
+ 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"))
@@ -604,13 +147,7 @@ onBeforeMount(() => {
})
onMounted(async () => {
- window.addEventListener("resize", debouncedRedraw)
-
- buildNamespaceCharts()
-})
-
-onBeforeUnmount(() => {
- window.removeEventListener("resize", debouncedRedraw)
+ await fetchAllData()
})
@@ -685,182 +222,23 @@ 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 a131ea59..b8d90618 100644
--- a/components/modules/rollup/RollupCharts.vue
+++ b/components/modules/rollup/RollupCharts.vue
@@ -1,8 +1,6 @@
+
@@ -986,292 +428,58 @@ 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
-
-
-
-
-
-
- {{ getXAxisLabels(true) }}
-
-
-
- {{ getXAxisLabels(false) }}
-
-
-
-
-
-
-
-
-
-
- {{ badgeText }}
-
-
-
-
- Usage
- {{ formatBytes(tooltipText) }}
-
-
-
-
-
-
-
-
-
-
- Blobs Count
-
-
-
-
- {{ abbreviate(Math.max(...pfbSeries.map((d) => d.value)), 0) }}
-
-
-
-
- {{ abbreviate(Math.round(Math.max(...pfbSeries.map((d) => d.value)) / 2), 0) }}
-
-
-
- 0
-
-
-
-
-
-
- {{ getXAxisLabels(true) }}
-
-
-
- {{ getXAxisLabels(false) }}
-
-
-
-
-
-
-
-
-
-
- {{ badgeText }}
-
-
-
-
- Count
- {{ abbreviate(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
-
-
-
-
-
-
- {{ getXAxisLabels(true) }}
-
-
-
- {{ getXAxisLabels(false) }}
-
-
-
-
-
-
-
-
-
-
- {{ badgeText }}
-
-
-
-
- Spent
- {{ tia(tooltipText) }} TIA
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
- 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`
+ }}
@@ -1279,158 +487,76 @@ onBeforeUnmount(() => {
- Grouping by day or month is only available for this chart.
+
+ Grouping by day or month is only available for this chart.
+
-
-
-
+
+
+
-
-
+
+
+
+
+ {{ selectedTvlDataSource?.title }}
+
- {{ selectedTvlDataSource?.title }}
+
-
-
+
+
+ Select TVL Data Source
-
-
- Select TVL Data Source
+
+
+
+
-
-
-
-
-
- {{ ds.title }}
-
+ {{ ds.title }}
+
-
+
+
-
-
-
-
-
-
-
-
- {{
- Math.max(...tvlSeries.map((d) => d.value)) < 1_000_000
- ? abbreviate(Math.max(...tvlSeries.map((d) => d.value)), 0)
- : abbreviate(Math.max(...tvlSeries.map((d) => d.value)))
- }}
- $
-
-
-
-
- {{
- Math.round(Math.max(...tvlSeries.map((d) => d.value)) / 2) < 1_000_000
- ? abbreviate(Math.round(Math.max(...tvlSeries.map((d) => d.value)) / 2), 0)
- : abbreviate(Math.round(Math.max(...tvlSeries.map((d) => d.value)) / 2))
- }}
- $
-
-
-
- 0
-
-
-
-
-
-
- {{ getXAxisLabels(true, true) }}
-
-
-
- {{ getXAxisLabels(false, true) }}
-
-
+
+
-
-
-
-
-
-
-
- {{ badgeText }}
-
-
-
-
-
- {{ abbreviate(tooltipText) }} $
-
-
-
-
-
-
-
-
+
+
-
@@ -1505,7 +631,7 @@ onBeforeUnmount(() => {
:class="$style.graph_bar"
:style="{
width: `${comparisonData[0]?.size_graph}%`,
- background: 'var(--mint)',
+ background: rollupColor,
marginRight: '4px',
}"
>
@@ -1533,7 +659,7 @@ onBeforeUnmount(() => {
:class="$style.graph_bar"
:style="{
width: `${comparisonData[0]?.pfb_graph}%`,
- background: 'var(--mint)',
+ background: rollupColor,
marginRight: '4px',
}"
>
@@ -1561,7 +687,7 @@ onBeforeUnmount(() => {
:class="$style.graph_bar"
:style="{
width: `${comparisonData[0]?.fee_graph}%`,
- background: 'var(--mint)',
+ background: rollupColor,
marginRight: '4px',
}"
>
@@ -1599,7 +725,7 @@ onBeforeUnmount(() => {
-
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..7be2c442
--- /dev/null
+++ b/scripts/generateBadgesList.js
@@ -0,0 +1,25 @@
+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)
+)
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..8854c104 100644
--- a/services/config.js
+++ b/services/config.js
@@ -39,7 +39,7 @@ export const useServerURL = () => {
return Server.API.mammoth
case "dev.celenium.io":
- return Server.API.dev
+ return Server.API.mainnet
default:
return Server.API.mainnet
@@ -66,7 +66,7 @@ export const useSocketURL = () => {
return Server.WSS.mammoth
case "dev.celenium.io":
- return Server.WSS.dev
+ return Server.WSS.mainnet
default:
return Server.WSS.mainnet
diff --git a/services/utils/amounts.js b/services/utils/amounts.js
index a0186e9d..d344c6fc 100644
--- a/services/utils/amounts.js
+++ b/services/utils/amounts.js
@@ -57,7 +57,7 @@ export const truncate = (num) => {
export const tia = (amount, decimal = 6) => {
if (!amount || !parseInt(amount)) return 0
- return truncateDecimalPart(parseInt(amount) / 1_000_000, decimal)
+ return truncateDecimalPart(parseInt(amount) / 1_000_000, decimal) + " TIA"
}
export const utia = (amount) => {
diff --git a/services/utils/entityCharts.js b/services/utils/entityCharts.js
new file mode 100644
index 00000000..f81260b8
--- /dev/null
+++ b/services/utils/entityCharts.js
@@ -0,0 +1,522 @@
+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
+ * @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, tooltipConfig, color = "var(--brand)") => {
+ 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
+ const showChart = metric === "tvl" ? MAX_VALUE > 1 : data.length
+
+ /** 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 (!showChart) return
+
+ onEnter()
+
+ const idx = bisect(data, x.invert(d3.pointer(event)[0]))
+
+ 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)
+ if (tooltipYOffset) tooltipYOffset.value = event.layerY
+ if (tooltipText) tooltipText.value = data[idx].value
+
+ if (tooltipEl && tooltipEl.value) {
+ 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?.timeframe
+ if (metric === "tvl" && ["hour", "week"].includes(selectedPeriod?.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 (!showChart) return
+
+ onLeave()
+
+ const { badgeText } = tooltipConfig
+ 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 (showChart) {
+ /** Chart Line */
+ const { loadLastValue } = tooltipConfig
+ let path1 = null
+ let path2 = null
+
+ const area = d3.area()
+ .x(d => x(d.date))
+ .y0(y(0))
+ .y1(d => y(d.value))
+
+ const clipId = `clip-${metric}-${Date.now()}`
+ const clipPath = svg.append("clipPath")
+ .attr("id", clipId)
+ const clipRect = clipPath.append("rect")
+ .attr("x", marginLeft)
+ .attr("y", 0)
+ .attr("width", 0)
+ .attr("height", height)
+
+ svg.append("path")
+ .datum(data)
+ .attr("fill", color)
+ .attr("fill-opacity", 0.1)
+ .attr("stroke", "none")
+ .attr("clip-path", `url(#${clipId})`)
+ .attr("d", area)
+
+ path1 = svg
+ .append("path")
+ .attr("fill", "none")
+ .attr("stroke", color)
+ .attr("stroke-width", 3)
+ .attr("stroke-linecap", "round")
+ .attr("stroke-linejoin", "round")
+ .attr("d", line(loadLastValue ? data.slice(0, data.length - 1) : data))
+
+ if (loadLastValue) {
+ // Create pattern
+ const defs = svg.append("defs")
+ const pattern = defs
+ .append("pattern")
+ .attr("id", `dashedPattern-${metric}`)
+ .attr("width", 8)
+ .attr("height", 2)
+ .attr("patternUnits", "userSpaceOnUse")
+ pattern.append("rect").attr("width", 4).attr("height", 2).attr("fill", color)
+ 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-${metric})`)
+ .attr("stroke-width", 3)
+ .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 ? (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) {
+ 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", color)
+ .attr("r", 3)
+ .attr("opacity", 0)
+
+ clipRect.transition()
+ .duration(totalDuration)
+ .ease(d3.easeLinear)
+ .attr("width", width - marginLeft)
+
+ 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-30)")
+ .style("font-size", "14px")
+ .text("No data available")
+ }
+
+ 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, tooltipConfig, color = "var(--brand)") => {
+ 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 ? 2 : 8)), 3)
+
+ const MAX_VALUE = d3.max(data, (d) => d.value) ? d3.max(data, (d) => d.value) : 1
+ const showChart = metric === "tvl" ? MAX_VALUE > 1 : data.length
+
+ /** Scale */
+ const xBand = d3
+ .scaleBand()
+ .domain(data.map((d) => new Date(d.date)))
+ .range([marginLeft, width - marginRight])
+ .padding(0.1)
+
+ 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 (!showChart) 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)"
+ }
+ })
+
+ 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)
+ if (tooltipYOffset) tooltipYOffset.value = event.layerY
+ if (tooltipText) tooltipText.value = data[idx].value
+
+ if (tooltipEl && tooltipEl.value) {
+ 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?.timeframe
+ if (metric === "tvl" && ["hour", "week"].includes(selectedPeriod?.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 (!showChart) return
+
+ onLeave()
+
+ const elements = document.querySelectorAll("[data-index]")
+ elements.forEach((el) => {
+ el.style.filter = ""
+ })
+
+ const { badgeText } = tooltipConfig
+ 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 (showChart) {
+ const { loadLastValue } = tooltipConfig
+ /** Chart Bars */
+ svg.append("defs")
+ .append("pattern")
+ .attr("id", `diagonal-stripe-${metric}`)
+ .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", color)
+
+ 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) => xBand(new Date(d.date)))
+ .attr("y", (d) => y(d.value))
+ .attr("width", xBand.bandwidth())
+ .attr("fill", (d, i) => (loadLastValue && i === data.length - 1 ? `url(#diagonal-stripe-${metric})` : color))
+ .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-30)")
+ .style("font-size", "14px")
+ .text("No data available")
+ }
+
+ 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..5f226fe2 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 "./entityCharts"
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