From be4e390a04fbe67570d1837cbc63250499b23342 Mon Sep 17 00:00:00 2001 From: GussevPM Date: Fri, 10 Jan 2025 12:09:59 +0100 Subject: [PATCH 01/24] Fix daily stats chart for Firefox --- .../stats/ParallelCoordinatesChart.vue | 22 ++++++++++--------- services/config.js | 4 ++-- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/components/modules/stats/ParallelCoordinatesChart.vue b/components/modules/stats/ParallelCoordinatesChart.vue index 6a11e15a..a0ebe42a 100644 --- a/components/modules/stats/ParallelCoordinatesChart.vue +++ b/components/modules/stats/ParallelCoordinatesChart.vue @@ -234,21 +234,23 @@ const buildChart = (chart, data) => { .style("opacity", 0.5) .style("filter", "brightness(0.6)") .style("transition", "all 0.5s ease") - // .attr("stroke-linecap", "round") .attr("stroke-linejoin", "round") .attr("d", path ) + .each(function () { + const length = this.getTotalLength(); + if (!isNaN(length) && length > 0) { + d3.select(this) + .attr("stroke-dasharray", length) + .attr("stroke-dashoffset", length) + .transition() + .duration(1000) + .ease(d3.easeCubic) + .attr("stroke-dashoffset", 0); + } + }) .on('mouseover', (event, d) => highlight(d.slug)) .on('mouseleave', (event, d) => unhighlight(d.slug)) .on('click', (event, d) => selectRollup(d.slug)) - .style("stroke-dasharray", function() { return this.getTotalLength(); }) - // .style("stroke-dashoffset", function() { - // return Math.random() > 0.5 ? this.getTotalLength() : -this.getTotalLength(); - // }) - .style("stroke-dashoffset", function() { return this.getTotalLength(); }) - .transition() - .duration(1000) - .ease(d3.easeCubic) - .style("stroke-dashoffset", 0) svg.selectAll("first-last-axis") .data(edgeAxis).enter() diff --git a/services/config.js b/services/config.js index dc7bff13..958b078c 100644 --- a/services/config.js +++ b/services/config.js @@ -37,7 +37,7 @@ export const useServerURL = () => { return Server.API.dev default: - return Server.API.dev + return Server.API.mainnet } } @@ -61,7 +61,7 @@ export const useSocketURL = () => { return Server.WSS.dev default: - return Server.WSS.dev + return Server.WSS.mainnet } } From 255510f71fb77b06a2d302b50143cc8af6c26edc Mon Sep 17 00:00:00 2001 From: GussevPM Date: Wed, 15 Jan 2025 20:23:01 +0100 Subject: [PATCH 02/24] Add bar charts for rollups and namespaces --- components/modules/gas/GasEfficiencyChart.vue | 10 +- components/modules/gas/GasPriceChart.vue | 8 +- components/modules/gas/GasPriceHeatmap.vue | 2 +- .../modules/namespace/NamespaceCharts.vue | 469 ++++++++++++--- components/modules/rollup/RollupCharts.vue | 555 +++++++++++++----- components/modules/stats/BarChart.vue | 2 +- 6 files changed, 816 insertions(+), 230 deletions(-) diff --git a/components/modules/gas/GasEfficiencyChart.vue b/components/modules/gas/GasEfficiencyChart.vue index a65043e0..58a409f6 100644 --- a/components/modules/gas/GasEfficiencyChart.vue +++ b/components/modules/gas/GasEfficiencyChart.vue @@ -152,14 +152,14 @@ const buildChart = (chartEl, data, onEnter, onLeave) => { /** Chart Line */ svg.append("path") .attr("fill", "none") - .attr("stroke", "var(--green)") + .attr("stroke", "var(--mint)") .attr("stroke-width", 2) .attr("stroke-linecap", "round") .attr("stroke-linejoin", "round") .attr("d", lineEfficiency(data.slice(0, 23))) svg.append("path") .attr("fill", "none") - .attr("stroke", "var(--green)") + .attr("stroke", "var(--mint)") .attr("stroke-width", 2) .attr("stroke-linecap", "round") .attr("stroke-linejoin", "round") @@ -187,7 +187,7 @@ const buildChart = (chartEl, data, onEnter, onLeave) => { svg.append("circle") .attr("cx", xScaleEfficiency(data[data.length - 1].date)) .attr("cy", yScaleEfficiency(data[data.length - 1].value)) - .attr("fill", "var(--green)") + .attr("fill", "var(--mint)") .attr("r", 3) if (chartEl.children[0]) chartEl.children[0].remove() @@ -307,7 +307,7 @@ onBeforeUnmount(() => { -
+
Efficiency @@ -420,7 +420,7 @@ onBeforeUnmount(() => { width: 6px; height: 6px; border-radius: 50px; - background: var(--green); + background: var(--mint); box-shadow: 0 0 0 4px rgba(10, 222, 113, 27%); diff --git a/components/modules/gas/GasPriceChart.vue b/components/modules/gas/GasPriceChart.vue index 09b88abc..1a9d106f 100644 --- a/components/modules/gas/GasPriceChart.vue +++ b/components/modules/gas/GasPriceChart.vue @@ -129,14 +129,14 @@ const buildChart = (chartEl, data, onEnter, onLeave) => { /** Chart Line */ svg.append("path") .attr("fill", "none") - .attr("stroke", "var(--green)") + .attr("stroke", "var(--mint)") .attr("stroke-width", 2) .attr("stroke-linecap", "round") .attr("stroke-linejoin", "round") .attr("d", line(data.slice(0, data.length - 1))) svg.append("path") .attr("fill", "none") - .attr("stroke", "var(--green)") + .attr("stroke", "var(--mint)") .attr("stroke-width", 2) .attr("stroke-linecap", "round") .attr("stroke-linejoin", "round") @@ -146,7 +146,7 @@ const buildChart = (chartEl, data, onEnter, onLeave) => { svg.append("circle") .attr("cx", x(data[data.length - 1].date)) .attr("cy", y(data[data.length - 1].value)) - .attr("fill", "var(--green)") + .attr("fill", "var(--mint)") .attr("r", 3) if (chartEl.children[0]) chartEl.children[0].remove() @@ -332,7 +332,7 @@ onBeforeUnmount(() => { width: 6px; height: 6px; border-radius: 50px; - background: var(--green); + background: var(--mint); box-shadow: 0 0 0 4px rgba(10, 222, 113, 27%); diff --git a/components/modules/gas/GasPriceHeatmap.vue b/components/modules/gas/GasPriceHeatmap.vue index b8c58459..39aabcf1 100644 --- a/components/modules/gas/GasPriceHeatmap.vue +++ b/components/modules/gas/GasPriceHeatmap.vue @@ -146,7 +146,7 @@ onMounted(async () => { & tbody { & tr td { - background: var(--neutral-green); + background: var(--mint); border: 1px solid var(--card-background); } diff --git a/components/modules/namespace/NamespaceCharts.vue b/components/modules/namespace/NamespaceCharts.vue index 80283fa5..0cf0fb16 100644 --- a/components/modules/namespace/NamespaceCharts.vue +++ b/components/modules/namespace/NamespaceCharts.vue @@ -7,6 +7,8 @@ import { useDebounceFn } from "@vueuse/core" /** UI */ 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" /** Services */ import { abbreviate, formatBytes } from "@/services/utils" @@ -21,6 +23,7 @@ const props = defineProps({ }, }) +/** Chart settings */ const selectedPeriodIdx = ref(1) const periods = ref([ { @@ -40,6 +43,24 @@ const periods = ref([ }, ]) const selectedPeriod = computed(() => periods.value[selectedPeriodIdx.value]) +const selectedChartView = ref("line") +const showLastValue = ref(true) + +const isOpen = ref(false) +const handleOpen = () => { + isOpen.value = true +} +const handleClose = () => { + isOpen.value = false +} + +const handleChangeChartView = () => { + if (selectedChartView.value === 'line') { + selectedChartView.value = 'bar' + } else { + selectedChartView.value = 'line' + } +} /** Charts */ const chartWrapperEl = ref() @@ -64,13 +85,37 @@ const badgeEl = ref() const badgeText = ref("") const badgeOffset = ref(0) -const buildChart = (chartEl, data, onEnter, onLeave) => { +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 = showLastValue.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 = showLastValue.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 = showLastValue.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 = 40 + const marginLeft = 52 const MAX_VALUE = d3.max(data, (d) => d.value) ? d3.max(data, (d) => d.value) : 1 @@ -106,17 +151,20 @@ const buildChart = (chartEl, data, onEnter, onLeave) => { } badgeText.value = - selectedPeriod.value.timeframe === "day" - ? DateTime.fromJSDate(data[idx].date).toFormat("LLL dd") - : DateTime.fromJSDate(data[idx].date).set({ minutes: 0 }).toFormat("hh:mm a") + 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 - if (idx < 2) { + const badgeWidth = badgeEl.value.getBoundingClientRect().width + if (tooltipXOffset.value - marginLeft < badgeWidth / 2) { badgeOffset.value = 0 - } else if (idx > selectedPeriod.value.value - 3) { - badgeOffset.value = badgeEl.value.getBoundingClientRect().width + } else if (badgeWidth + tooltipXOffset.value > width) { + badgeOffset.value = Math.abs(width - (badgeWidth + tooltipXOffset.value)) + ((data.length - 1 - idx) * 2) } else { - badgeOffset.value = badgeEl.value.getBoundingClientRect().width / 2 + badgeOffset.value = badgeWidth / 2 } } const onPointerleft = () => { @@ -157,27 +205,225 @@ const buildChart = (chartEl, data, onEnter, onLeave) => { .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(showLastValue.value ? data.slice(0, data.length - 1) : data)) + + if (showLastValue.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 = showLastValue.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 (showLastValue.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])) + 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(--brand)") + .attr("stroke", "var(--op-10)") .attr("stroke-width", 2) - .attr("stroke-linecap", "round") - .attr("stroke-linejoin", "round") - .attr("d", line(data.slice(0, data.length - 1))) + .attr("d", `M${marginLeft},${height - marginBottom + 2} L${marginLeft},${height - marginBottom - 5}`) svg.append("path") .attr("fill", "none") - .attr("stroke", "var(--brand)") + .attr("stroke", "var(--op-10)") .attr("stroke-width", 2) - .attr("stroke-linecap", "round") - .attr("stroke-linejoin", "round") - .attr("stroke-dasharray", "8") - .attr("d", line(data.slice(data.length - 2, data.length))) + .attr("d", `M${width - 1},${height - marginBottom + 2} L${width - 1},${height - marginBottom - 5}`) - 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) + /** 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) => (showLastValue.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()) @@ -248,22 +494,41 @@ const getPfbSeries = async () => { } } -const buildNamespaceCharts = async () => { - await getSizeSeries() - buildChart( - sizeSeriesChartEl.value.wrapper, - sizeSeries.value, - () => (showSeriesTooltip.value = true), - () => (showSeriesTooltip.value = false), - ) +const buildNamespaceCharts = async (loadData = true) => { + if (loadData) { + await getSizeSeries() + await getPfbSeries() + } - await getPfbSeries() - buildChart( - pfbSeriesChartEl.value.wrapper, - pfbSeries.value, - () => (showPfbTooltip.value = true), - () => (showPfbTooltip.value = false), - ) + if (selectedChartView.value === "line") { + buildLineChart( + sizeSeriesChartEl.value.wrapper, + showLastValue.value ? sizeSeries.value : sizeSeries.value.slice(0, sizeSeries.value.length - 1), + () => (showSeriesTooltip.value = true), + () => (showSeriesTooltip.value = false), + ) + buildLineChart( + pfbSeriesChartEl.value.wrapper, + showLastValue.value ? pfbSeries.value : pfbSeries.value.slice(0, pfbSeries.value.length - 1), + () => (showPfbTooltip.value = true), + () => (showPfbTooltip.value = false), + ) + } else { + buildBarChart( + sizeSeriesChartEl.value.wrapper, + showLastValue.value ? sizeSeries.value : sizeSeries.value.slice(0, sizeSeries.value.length - 1), + () => (showSeriesTooltip.value = true), + () => (showSeriesTooltip.value = false), + "size", + ) + buildBarChart( + pfbSeriesChartEl.value.wrapper, + showLastValue.value ? pfbSeries.value : pfbSeries.value.slice(0, pfbSeries.value.length - 1), + () => (showPfbTooltip.value = true), + () => (showPfbTooltip.value = false), + "pfb", + ) + } } watch( @@ -273,6 +538,13 @@ watch( }, ) +watch( + () => [selectedChartView.value, showLastValue.value], + () => { + buildNamespaceCharts(false) + } +) + const debouncedRedraw = useDebounceFn((e) => { buildNamespaceCharts() }, 500) @@ -296,21 +568,64 @@ onBeforeUnmount(() => { Analytics - - - - + + @@ -353,28 +668,23 @@ onBeforeUnmount(() => { - - {{ - DateTime.now() - .minus({ days: selectedPeriod.value - 1 }) - .toFormat("LLL dd") - }} - - - {{ DateTime.now().minus({ hours: selectedPeriod.value - 1 }).set({ minutes: 0 }).toFormat("hh:mm a") }} + + {{ xAxisLabels.firstDate }} - {{ selectedPeriod.timeframe === "day" ? "Today" : "Now" }} + + {{ xAxisLabels.lastDate }} +
-
-
+
{ - - {{ - DateTime.now() - .minus({ days: selectedPeriod.value - 1 }) - .toFormat("LLL dd") - }} - - - {{ DateTime.now().minus({ hours: selectedPeriod.value - 1 }).set({ minutes: 0 }).toFormat("hh:mm a") }} + + {{ xAxisLabels.firstDate }} - {{ selectedPeriod.timeframe === "day" ? "Today" : "Now" }} + + {{ xAxisLabels.lastDate }} +
-
-
+
{ padding: 0 12px; } +.setting_item { + min-height: 24px; +} + +.chart_selector { + padding: 4px 6px 4px 6px; + box-shadow: inset 0 0 0 1px var(--op-10); + border-radius: 5px; + cursor: pointer; + transition: all 1s ease-in-out; +} + .data { border-radius: 4px 4px 8px 8px; background: var(--card-background); diff --git a/components/modules/rollup/RollupCharts.vue b/components/modules/rollup/RollupCharts.vue index ebbfbecc..fc4f4518 100644 --- a/components/modules/rollup/RollupCharts.vue +++ b/components/modules/rollup/RollupCharts.vue @@ -9,6 +9,7 @@ import Button from "@/components/ui/Button.vue" import { Dropdown, DropdownItem } from "@/components/ui/Dropdown" import Input from "@/components/ui/Input.vue" import Popover from "@/components/ui/Popover.vue" +import Toggle from "@/components/ui/Toggle.vue" /** Services */ import { abbreviate, formatBytes, sortArrayOfObjects, spaces, tia } from "@/services/utils" @@ -24,6 +25,7 @@ const props = defineProps({ }, }) +/** Chart settings */ const selectedPeriodIdx = ref(0) const periods = ref([ { @@ -48,6 +50,24 @@ const periods = ref([ }, ]) const selectedPeriod = computed(() => periods.value[selectedPeriodIdx.value]) +const selectedChartView = ref("line") +const showLastValue = ref(true) + +const isOpen = ref(false) +const handleOpen = () => { + isOpen.value = true +} +const handleClose = () => { + isOpen.value = false +} + +const handleChangeChartView = () => { + if (selectedChartView.value === 'line') { + selectedChartView.value = 'bar' + } else { + selectedChartView.value = 'line' + } +} /** Charts */ const chartWrapperEl = ref() @@ -55,7 +75,7 @@ const sizeSeriesChartEl = ref() const pfbSeriesChartEl = ref() const feeSeriesChartEl = ref() const comparisonChartEl = ref() -const barWidth = ref(0) +const comparisonBarWidth = ref(0) /** Data */ const isLoading = ref(false) @@ -70,7 +90,6 @@ const selectedRollup = ref() const showSeriesTooltip = ref(false) const showPfbTooltip = ref(false) const showFeeTooltip = ref(false) -const showComparisonTooltip = ref(false) const tooltipEl = ref() const tooltipXOffset = ref(0) const tooltipYOffset = ref(0) @@ -82,13 +101,37 @@ const badgeEl = ref() const badgeText = ref("") const badgeOffset = ref(0) -const buildChart = (chartEl, data, onEnter, onLeave) => { +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 = showLastValue.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 = showLastValue.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 = showLastValue.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 = 40 + const marginLeft = 52 const MAX_VALUE = d3.max(data, (d) => d.value) ? d3.max(data, (d) => d.value) : 1 @@ -131,12 +174,13 @@ const buildChart = (chartEl, data, onEnter, onLeave) => { : DateTime.fromJSDate(data[idx].date).set({ minutes: 0 }).toFormat("hh:mm a") if (!badgeEl.value) return - if (idx < 2) { + const badgeWidth = badgeEl.value.getBoundingClientRect().width + if (tooltipXOffset.value - marginLeft < badgeWidth / 2) { badgeOffset.value = 0 - } else if (idx > selectedPeriod.value.value - 3) { - badgeOffset.value = badgeEl.value.getBoundingClientRect().width + } else if (badgeWidth + tooltipXOffset.value > width) { + badgeOffset.value = Math.abs(width - (badgeWidth + tooltipXOffset.value)) + ((data.length - 1 - idx) * 2) } else { - badgeOffset.value = badgeEl.value.getBoundingClientRect().width / 2 + badgeOffset.value = badgeWidth / 2 } } const onPointerleft = () => { @@ -177,27 +221,225 @@ const buildChart = (chartEl, data, onEnter, onLeave) => { .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(showLastValue.value ? data.slice(0, data.length - 1) : data)) + + if (showLastValue.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 = showLastValue.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 (showLastValue.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])) + 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(--brand)") + .attr("stroke", "var(--op-10)") .attr("stroke-width", 2) - .attr("stroke-linecap", "round") - .attr("stroke-linejoin", "round") - .attr("d", line(data.slice(0, data.length - 1))) + .attr("d", `M${marginLeft},${height - marginBottom + 2} L${marginLeft},${height - marginBottom - 5}`) svg.append("path") .attr("fill", "none") - .attr("stroke", "var(--brand)") + .attr("stroke", "var(--op-10)") .attr("stroke-width", 2) - .attr("stroke-linecap", "round") - .attr("stroke-linejoin", "round") - .attr("stroke-dasharray", "8") - .attr("d", line(data.slice(data.length - 2, data.length))) + .attr("d", `M${width - 1},${height - marginBottom + 2} L${width - 1},${height - marginBottom - 5}`) - 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) + /** 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) => (showLastValue.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()) @@ -362,39 +604,64 @@ const filteredRollupsList = computed(() => { return rollupsList.value.filter((r) => r.name.toLowerCase().includes(searchTerm.value.trim().toLowerCase())) }) -const buildRollupCharts = async () => { +const buildRollupCharts = async (loadData = true) => { isLoading.value = true - await getRollupsList() - if (!selectedRollup.value) { - selectedRollup.value = rollupsList.value[0] - } - - barWidth.value = comparisonChartEl.value.wrapper.getBoundingClientRect().width + if (loadData) { + await getRollupsList() + if (!selectedRollup.value) { + selectedRollup.value = rollupsList.value[0] + } - await getSizeSeries() - buildChart( - sizeSeriesChartEl.value.wrapper, - sizeSeries.value, - () => (showSeriesTooltip.value = true), - () => (showSeriesTooltip.value = false), - ) + comparisonBarWidth.value = comparisonChartEl.value.wrapper.getBoundingClientRect().width - await getPfbSeries() - buildChart( - pfbSeriesChartEl.value.wrapper, - pfbSeries.value, - () => (showPfbTooltip.value = true), - () => (showPfbTooltip.value = false), - ) + await getSizeSeries() + await getPfbSeries() + await getFeeSeries() + } - await getFeeSeries() - buildChart( - feeSeriesChartEl.value.wrapper, - feeSeries.value, - () => (showFeeTooltip.value = true), - () => (showFeeTooltip.value = false), - ) + if (selectedChartView.value === "line") { + buildLineChart( + sizeSeriesChartEl.value.wrapper, + showLastValue.value ? sizeSeries.value : sizeSeries.value.slice(0, sizeSeries.value.length - 1), + () => (showSeriesTooltip.value = true), + () => (showSeriesTooltip.value = false), + ) + buildLineChart( + pfbSeriesChartEl.value.wrapper, + showLastValue.value ? pfbSeries.value : pfbSeries.value.slice(0, pfbSeries.value.length - 1), + () => (showPfbTooltip.value = true), + () => (showPfbTooltip.value = false), + ) + buildLineChart( + feeSeriesChartEl.value.wrapper, + showLastValue.value ? feeSeries.value : feeSeries.value.slice(0, feeSeries.value.length - 1), + () => (showFeeTooltip.value = true), + () => (showFeeTooltip.value = false), + ) + } else { + buildBarChart( + sizeSeriesChartEl.value.wrapper, + showLastValue.value ? sizeSeries.value : sizeSeries.value.slice(0, sizeSeries.value.length - 1), + () => (showSeriesTooltip.value = true), + () => (showSeriesTooltip.value = false), + "size", + ) + buildBarChart( + pfbSeriesChartEl.value.wrapper, + showLastValue.value ? pfbSeries.value : pfbSeries.value.slice(0, pfbSeries.value.length - 1), + () => (showPfbTooltip.value = true), + () => (showPfbTooltip.value = false), + "pfb", + ) + buildBarChart( + feeSeriesChartEl.value.wrapper, + showLastValue.value ? feeSeries.value : feeSeries.value.slice(0, feeSeries.value.length - 1), + () => (showFeeTooltip.value = true), + () => (showFeeTooltip.value = false), + "fee", + ) + } await prepareComparisonData() @@ -410,6 +677,13 @@ watch( }, ) +watch( + () => [selectedChartView.value, showLastValue.value], + () => { + buildRollupCharts(false) + } +) + watch( () => selectedRollup.value, () => { @@ -443,21 +717,64 @@ onBeforeUnmount(() => { Analytics - - - - + + @@ -500,44 +817,24 @@ onBeforeUnmount(() => { - - - {{ - DateTime.now() - .minus({ months: selectedPeriod.value - 1 }) - .toFormat("LLL y") - }} - - - {{ DateTime.now().toFormat("LLL") }} - - + - {{ - DateTime.now() - .minus({ days: selectedPeriod.value - 1 }) - .toFormat("LLL dd") - }} + {{ xAxisLabels.firstDate }} - Today - - - {{ DateTime.now().minus({ hours: selectedPeriod.value }).set({ minutes: 0 }).toFormat("hh:mm a") }} + {{ xAxisLabels.lastDate }} - - Now
-
-
+
{ - - - {{ - DateTime.now() - .minus({ months: selectedPeriod.value - 1 }) - .toFormat("LLL y") - }} - - - {{ DateTime.now().toFormat("LLL") }} - - + - {{ - DateTime.now() - .minus({ days: selectedPeriod.value - 1 }) - .toFormat("LLL dd") - }} + {{ xAxisLabels.firstDate }} - Today - - - {{ DateTime.now().minus({ hours: selectedPeriod.value }).set({ minutes: 0 }).toFormat("hh:mm a") }} + {{ xAxisLabels.lastDate }} - - Now
-
-
+
{ - - - {{ - DateTime.now() - .minus({ months: selectedPeriod.value - 1 }) - .toFormat("LLL y") - }} - - - {{ DateTime.now().toFormat("LLL") }} - - + - {{ - DateTime.now() - .minus({ days: selectedPeriod.value - 1 }) - .toFormat("LLL dd") - }} + {{ xAxisLabels.firstDate }} - Today - - - {{ DateTime.now().minus({ hours: selectedPeriod.value }).set({ minutes: 0 }).toFormat("hh:mm a") }} + {{ xAxisLabels.lastDate }} - - Now
-
-
+
{ Size - +
Blobs - +
Fee - +
{ svg.append("circle") .attr("cx", xScaleEfficiency(data[data.length - 1].date)) .attr("cy", yScaleEfficiency(data[data.length - 1].value)) - .attr("fill", "var(--mint)") + .attr("fill", "var(--brand)") .attr("r", 3) if (chartEl.children[0]) chartEl.children[0].remove() @@ -307,7 +307,7 @@ onBeforeUnmount(() => { -
+
Efficiency @@ -420,7 +420,7 @@ onBeforeUnmount(() => { width: 6px; height: 6px; border-radius: 50px; - background: var(--mint); + background: var(--brand); box-shadow: 0 0 0 4px rgba(10, 222, 113, 27%); diff --git a/components/modules/gas/GasPriceChart.vue b/components/modules/gas/GasPriceChart.vue index 1a9d106f..de38f723 100644 --- a/components/modules/gas/GasPriceChart.vue +++ b/components/modules/gas/GasPriceChart.vue @@ -129,14 +129,14 @@ const buildChart = (chartEl, data, onEnter, onLeave) => { /** Chart Line */ svg.append("path") .attr("fill", "none") - .attr("stroke", "var(--mint)") + .attr("stroke", "var(--brand)") .attr("stroke-width", 2) .attr("stroke-linecap", "round") .attr("stroke-linejoin", "round") .attr("d", line(data.slice(0, data.length - 1))) svg.append("path") .attr("fill", "none") - .attr("stroke", "var(--mint)") + .attr("stroke", "var(--brand)") .attr("stroke-width", 2) .attr("stroke-linecap", "round") .attr("stroke-linejoin", "round") @@ -146,7 +146,7 @@ const buildChart = (chartEl, data, onEnter, onLeave) => { svg.append("circle") .attr("cx", x(data[data.length - 1].date)) .attr("cy", y(data[data.length - 1].value)) - .attr("fill", "var(--mint)") + .attr("fill", "var(--brand)") .attr("r", 3) if (chartEl.children[0]) chartEl.children[0].remove() @@ -332,7 +332,7 @@ onBeforeUnmount(() => { width: 6px; height: 6px; border-radius: 50px; - background: var(--mint); + background: var(--brand); box-shadow: 0 0 0 4px rgba(10, 222, 113, 27%); diff --git a/components/modules/gas/GasPriceHeatmap.vue b/components/modules/gas/GasPriceHeatmap.vue index 39aabcf1..6347a59f 100644 --- a/components/modules/gas/GasPriceHeatmap.vue +++ b/components/modules/gas/GasPriceHeatmap.vue @@ -146,7 +146,7 @@ onMounted(async () => { & tbody { & tr td { - background: var(--mint); + background: var(--brand); border: 1px solid var(--card-background); } @@ -155,7 +155,7 @@ onMounted(async () => { } & tr td:hover { - outline: 1px solid #fff; + outline: 1px solid var(--txt-secondary); } & tr th { diff --git a/components/widgets/BlobsWidget.vue b/components/widgets/BlobsWidget.vue index 351f379e..1371d43e 100644 --- a/components/widgets/BlobsWidget.vue +++ b/components/widgets/BlobsWidget.vue @@ -112,7 +112,7 @@ const selectDay = (d) => { @click="selectDay(day)" :class="[$style.day, day?.value > 0 && $style.shadow]" :style="{ - background: parseInt(day?.value) > 0 ? `rgb(10, 219, 111)` : 'var(--op-10)', + background: parseInt(day?.value) > 0 ? `var(--brand)` : 'var(--op-10)', opacity: calculateOpacity(day?.value), }" /> diff --git a/components/widgets/BlockWidget.vue b/components/widgets/BlockWidget.vue index 5e467099..3092c6bc 100644 --- a/components/widgets/BlockWidget.vue +++ b/components/widgets/BlockWidget.vue @@ -110,7 +110,7 @@ onBeforeUnmount(() => { Block - {{ comma(lastBlock.height) }} + {{ comma(lastBlock.height) }} @@ -253,7 +253,8 @@ onBeforeUnmount(() => { width: 100%; - background: var(--block-progress-fill-background); + background: var(--dark-mint); + /* background: var(block-progress-fill-background); */ z-index: -1; diff --git a/components/widgets/GasWidget.vue b/components/widgets/GasWidget.vue index bf84b309..e92581ed 100644 --- a/components/widgets/GasWidget.vue +++ b/components/widgets/GasWidget.vue @@ -119,7 +119,8 @@ onMounted(async () => { padding: 0 12px; &.fast { - background: linear-gradient(rgba(10, 219, 111, 25%), rgba(10, 219, 111, 10%)); + /* background: linear-gradient(rgba(10, 219, 111, 25%), rgba(10, 219, 111, 10%)); */ + background: linear-gradient(var(--dark-mint), rgba(10, 219, 111, 10%)); box-shadow: inset 0 0 0 1px rgba(10, 219, 111, 50%); } diff --git a/components/widgets/StakingWidget.vue b/components/widgets/StakingWidget.vue index e0de18c5..76ce59e3 100644 --- a/components/widgets/StakingWidget.vue +++ b/components/widgets/StakingWidget.vue @@ -35,7 +35,7 @@ const validatorsGraph = ref([ title: "active", count: 0, width: 0, - color: "var(--validator-active)", + color: "var(--brand)", }, { title: "inactive", @@ -253,7 +253,7 @@ onMounted(() => { border-radius: 2px; - background: linear-gradient(90deg, var(--staking) var(--percentStaking), var(--supply) var(--percentStaking)); + background: linear-gradient(90deg, var(--brand) var(--percentStaking), var(--supply) var(--percentStaking)); } .validator_bar { diff --git a/components/widgets/TransactionsWidget.vue b/components/widgets/TransactionsWidget.vue index 3ceba6dd..45c13d57 100644 --- a/components/widgets/TransactionsWidget.vue +++ b/components/widgets/TransactionsWidget.vue @@ -223,7 +223,7 @@ const getSectorName = (item) => { .hour.current { .bar { - background: var(--blue); + background: var(--supply); animation: blink 1.5s ease infinite; } } @@ -249,7 +249,7 @@ const getSectorName = (item) => { background: var(--txt-tertiary); &.green { - background: var(--green); + background: var(--brand); } } From b902f903b15c0ca8f97fd94a7a7620bef9148697 Mon Sep 17 00:00:00 2001 From: GussevPM Date: Sat, 18 Jan 2025 18:54:55 +0100 Subject: [PATCH 04/24] Add timeframe selection and saving chart setting --- app.vue | 2 + components/DatePicker.vue | 45 ++-- components/LeftSidebar.vue | 6 + .../modules/namespace/NamespaceCharts.vue | 75 +++--- components/modules/navigation/NavLink.vue | 69 +++++- components/modules/rollup/RollupCharts.vue | 83 ++++--- components/modules/stats/BarChart.vue | 13 +- components/modules/stats/LineChart.vue | 11 +- components/widgets/BlockWidget.vue | 5 +- pages/stats/[metric].vue | 221 +++++++++++++++--- pages/txs.vue | 51 ++-- pages/validators.vue | 4 + services/constants/stats.js | 27 +++ store/settings.js | 22 +- 14 files changed, 498 insertions(+), 136 deletions(-) diff --git a/app.vue b/app.vue index 8e421966..9f2c4ef7 100644 --- a/app.vue +++ b/app.vue @@ -47,6 +47,8 @@ onMounted(async () => { nodeStore.settings = JSON.parse(localStorage.nodeSettings) } + settingsStore.init() + const runtimeConfig = useRuntimeConfig() amp.init(runtimeConfig.public.AMP) diff --git a/components/DatePicker.vue b/components/DatePicker.vue index c148d505..40a82128 100644 --- a/components/DatePicker.vue +++ b/components/DatePicker.vue @@ -1,6 +1,5 @@ diff --git a/components/modules/stats/LineChart.vue b/components/modules/stats/LineChart.vue index 78242717..ca62f476 100644 --- a/components/modules/stats/LineChart.vue +++ b/components/modules/stats/LineChart.vue @@ -252,7 +252,6 @@ const buildChart = (chart, cData, pData, onEnter, onLeave) => { const drawChart = () => { currentData.value.color = "var(--mint)" prevData.value.color = "var(--txt-tertiary)" - buildChart( chartEl.value.wrapper, currentData.value, @@ -265,12 +264,16 @@ const drawChart = () => { watch( () => [currentData.value, prevData.value], () => { - drawChart() + if (chartEl?.value?.wrapper) { + drawChart() + } }, ) -onMounted(async () => { - drawChart() +onMounted(() => { + if (chartEl?.value?.wrapper) { + drawChart() + } }) diff --git a/components/widgets/BlockWidget.vue b/components/widgets/BlockWidget.vue index 3092c6bc..e0d8d424 100644 --- a/components/widgets/BlockWidget.vue +++ b/components/widgets/BlockWidget.vue @@ -253,8 +253,9 @@ onBeforeUnmount(() => { width: 100%; - background: var(--dark-mint); - /* background: var(block-progress-fill-background); */ + /* background: var(--dark-mint); */ + background: var(--neutral-mint); + /* background: var(--block-progress-fill-background); */ z-index: -1; diff --git a/pages/stats/[metric].vue b/pages/stats/[metric].vue index ad1014f7..5d16249a 100644 --- a/pages/stats/[metric].vue +++ b/pages/stats/[metric].vue @@ -3,7 +3,7 @@ import { DateTime } from "luxon" /** Stats Components/Constants */ -import { getSeriesByPage, STATS_PERIODS } from "@/services/constants/stats.js" +import { getSeriesByPage, STATS_PERIODS, STATS_TIMEFRAMES } from "@/services/constants/stats.js" import BarChart from "@/components/modules/stats/BarChart.vue" import LineChart from "@/components/modules/stats/LineChart.vue" import SquareSizeChart from "@/components/modules/stats/SquareSizeChart.vue" @@ -23,13 +23,14 @@ import Popover from "@/components/ui/Popover.vue" import Toggle from "@/components/ui/Toggle.vue" /** Store */ -/** Store */ +import { useCacheStore } from "@/store/cache" import { useModalsStore } from "@/store/modals" import { useNotificationsStore } from "@/store/notifications" -import { useCacheStore } from "@/store/cache" +import { useSettingsStore } from "@/store/settings" +const cacheStore = useCacheStore() const modalsStore = useModalsStore() const notificationsStore = useNotificationsStore() -const cacheStore = useCacheStore() +const settingsStore = useSettingsStore() const route = useRoute() const router = useRouter() @@ -96,12 +97,46 @@ useHead({ const periods = ref(STATS_PERIODS) const selectedPeriod = ref(periods.value[2]) +const selectedTimeframe = ref(STATS_TIMEFRAMES.find(tf => tf.timeframe === selectedPeriod.value.timeframe)) +const timeframes = computed(() => { + let res = [] + + for (const tf of STATS_TIMEFRAMES) { + const pointCount = Math.floor(DateTime.fromSeconds(filters.to).diff(DateTime.fromSeconds(filters.from), `${tf.timeframe}s`)[`${tf.timeframe}s`]) + 1 + + if (pointCount > 1 && pointCount < 100) { + res.push(tf) + } + } + + return res +}) +const timeframesStyles = computed(() => { + const len = timeframes.value.length + if (!len) return { background: "var(--op-5)" } + + const segment = 100 / len + const index = timeframes.value.findIndex(tf => tf.timeframe === selectedTimeframe.value.timeframe) + const start = segment * index + const end = start + segment + + return { background: `linear-gradient(to right, transparent ${start}%, var(--op-5) ${start}%, var(--op-5) ${end}%, transparent ${end}%)` } +}) + const currentData = ref([]) const prevData = ref([]) -const chartView = ref('line') +const chartView = ref("line") const loadPrevData = ref(true) const loadLastValue = ref(true) +const updateUserSettings = () => { + settingsStore.chart = { + ...settingsStore.chart, + view: chartView.value, + loadPrevData: loadPrevData.value, + loadLastValue: loadLastValue.value, + } +} const filters = reactive({}) @@ -109,9 +144,9 @@ const setDefaultFilters = () => { filters.timeframe = selectedPeriod.value.timeframe filters.periodValue = selectedPeriod.value.value filters.from = parseInt(DateTime.now().startOf('day').minus({ - days: selectedPeriod.value.timeframe === "day" ? selectedPeriod.value.value - 1 : 0, // ?? - hours: selectedPeriod.value.timeframe === "hour" ? selectedPeriod.value.value : 0, - }).ts / 1_000) + days: selectedPeriod.value.timeframe === "day" ? selectedPeriod.value.value - 1 : 0, + hours: selectedPeriod.value.timeframe === "hour" ? selectedPeriod.value.value : 0, + }).ts / 1_000) filters.to = parseInt(DateTime.now().endOf('day').ts / 1_000) } @@ -126,26 +161,27 @@ const handleChangeChartView = () => { } const isLoading = ref(false) -const getData = async () => { - isLoading.value = true - +const fetchData = async (from, to) => { let data = [] - if (series.value.aggregate !== 'cumulative') { data = (await fetchSeries({ table: series.value.name, - period: filters.timeframe, - from: loadPrevData.value ? parseInt(DateTime.fromSeconds(filters.from).minus({ - hours: filters.timeframe === "hour" ? filters.periodValue : 0, - days: filters.timeframe === "day" ? filters.periodValue : 0, - weeks: filters.timeframe === "week" ? filters.periodValue : 0, - }).ts / 1_000) : filters.from, - to: filters.to + period: selectedTimeframe.value.timeframe, + from: from + ? from + : loadPrevData.value + ? parseInt(DateTime.fromSeconds(filters.from).minus({ + hours: filters.timeframe === "hour" ? filters.periodValue : 0, + days: filters.timeframe === "day" ? filters.periodValue : 0, + weeks: filters.timeframe === "week" ? filters.periodValue : 0, + }).ts / 1_000) + : filters.from, + to: to ? to : filters.to })).reverse() } else { data = await fetchSeriesCumulative({ name: series.value.name, - period: filters.timeframe, + period: selectedTimeframe.value.timeframe, from: loadPrevData.value ? parseInt(DateTime.fromSeconds(filters.from).minus({ hours: filters.timeframe === "hour" ? filters.periodValue : 0, days: filters.timeframe === "day" ? filters.periodValue : 0, @@ -155,13 +191,64 @@ const getData = async () => { }) } + return data +} +const getData = async () => { + isLoading.value = true + + let data = await fetchData() if (data.length) { + data.reverse() if (loadPrevData.value) { - prevData.value = data.slice(0, filters.periodValue).map((s) => ({ date: DateTime.fromISO(s.time).toJSDate(), value: parseFloat(s.value) })) - currentData.value = data.slice(filters.periodValue, data.length).map((s) => ({ date: DateTime.fromISO(s.time).toJSDate(), value: parseFloat(s.value) })) + if (selectedTimeframe.value.timeframe !== filters.timeframe) { + if (data.length % 2 > 0) { + const from = parseInt(DateTime.fromISO(data[data.length - 1].time) + .minus({ + hours: selectedTimeframe.value.timeframe === "hour" ? 1 : 0, + days: selectedTimeframe.value.timeframe === "day" ? 1 : 0, + weeks: selectedTimeframe.value.timeframe === "week" ? 1 : 0, + months: selectedTimeframe.value.timeframe === "month" ? 1 : 0 + }).ts / 1_000 + ) + const to = parseInt(DateTime.fromISO(data[data.length - 1].time).ts / 1_000) + let addData = await fetchData(from, to) + if (!addData.length) { + addData = [{ + date: DateTime.fromISO(data[data.length - 1].time) + .minus({ + hours: filters.timeframe === "hour" ? 1 : 0, + days: filters.timeframe === "day" ? 1 : 0, + weeks: filters.timeframe === "week" ? 1 : 0, + }) + .toJSDate(), + value: 0, + }] + } + data.push(addData[0]) + } + + currentData.value = data.slice(0, data.length / 2).map((s) => ({ date: DateTime.fromISO(s.time).toJSDate(), value: parseFloat(s.value) })).reverse() + prevData.value = data.slice(data.length / 2, data.length).map((s) => ({ date: DateTime.fromISO(s.time).toJSDate(), value: parseFloat(s.value) })).reverse() + } else { + currentData.value = data.slice(0, filters.periodValue).map((s) => ({ date: DateTime.fromISO(s.time).toJSDate(), value: parseFloat(s.value) })).reverse() + prevData.value = data.slice(filters.periodValue, data.length).map((s) => ({ date: DateTime.fromISO(s.time).toJSDate(), value: parseFloat(s.value) })).reverse() + while (prevData.value.length < currentData.value.length) { + prevData.value.unshift({ + date: DateTime.fromJSDate(prevData.value[0]?.date) + .minus({ + hours: filters.timeframe === "hour" ? 1 : 0, + days: filters.timeframe === "day" ? 1 : 0, + weeks: filters.timeframe === "week" ? 1 : 0, + }) + .toJSDate(), + value: 0 + }) + } + } } else { + // currentData.value = data.slice(0, filters.periodValue).map((s) => ({ date: DateTime.fromISO(s.time).toJSDate(), value: parseFloat(s.value) })).reverse() + currentData.value = data.map((s) => ({ date: DateTime.fromISO(s.time).toJSDate(), value: parseFloat(s.value) })).reverse() prevData.value = [] - currentData.value = data.slice(0, filters.periodValue).map((s) => ({ date: DateTime.fromISO(s.time).toJSDate(), value: parseFloat(s.value) })) } } @@ -185,21 +272,38 @@ const handleClose = () => { } const handleUpdateDate = async (event) => { + isLoading.value = true + if (event.from && event.to) { - let daysDiff = Math.round(DateTime.fromSeconds(event.to).diff(DateTime.fromSeconds(event.from), 'days').days) + let from = event.from + let to = event.to + + let daysDiff = Math.round(DateTime.fromSeconds(to).diff(DateTime.fromSeconds(from), 'days').days) if (daysDiff < 7) { filters.timeframe = 'hour' - filters.periodValue = Math.round(DateTime.fromSeconds(event.to).diff(DateTime.fromSeconds(event.from), 'hours').hours) + filters.periodValue = Math.round(DateTime.fromSeconds(to).diff(DateTime.fromSeconds(from), 'hours').hours) } else if (daysDiff < 50) { filters.timeframe = 'day' filters.periodValue = daysDiff } else { filters.timeframe = 'week' - filters.periodValue = Math.round(daysDiff / 7) + filters.periodValue = Math.ceil(daysDiff / 7) + } + + if (filters.timeframe === 'hour') { + const hoursDiff = Math.round(DateTime.fromSeconds(Math.min(to, DateTime.now().ts / 1_000)).diff(DateTime.fromSeconds(from), 'hours').hours) + if (hoursDiff < filters.periodValue) { + from = parseInt( + DateTime.fromSeconds(Math.min(to, DateTime.now().ts / 1_000)) + .minus({ hours: filters.periodValue }) + .ts / 1_000 + ) + } } - filters.from = event.from - filters.to = event.to + filters.from = from + filters.to = to + selectedTimeframe.value = timeframes.value.find(tf => tf.timeframe === filters.timeframe) await getData() } else if (event.clear) { @@ -209,6 +313,10 @@ const handleUpdateDate = async (event) => { } } +const handleTimeframeUpdate = (tf) => { + selectedTimeframe.value = tf +} + const handleCSVDownload = async () => { let data = [...series.value.currentData, ...series.value.prevData] let csvHeaders = 'timestamp,value\n' @@ -278,6 +386,29 @@ watch( } }, ) + +watch( + () => selectedTimeframe.value, + async () => { + if (!isLoading.value) { + await getData() + } + } +) + +watch( + () => [chartView.value, loadLastValue.value, loadPrevData.value], + () => { + updateUserSettings() + } +) + +onBeforeMount(() => { + const settings = JSON.parse(localStorage.getItem("settings")) + chartView.value = settings?.chart?.view || "line" + loadPrevData.value = settings?.chart?.loadPrevData + loadLastValue.value = settings?.chart?.loadLastValue +}) @@ -401,6 +554,18 @@ watch( transition: all 1s ease-in-out; } +.groupping_selector { + padding: 4px 6px 4px 6px; + box-shadow: inset 0 0 0 1px var(--op-10); + border-radius: 5px; + cursor: pointer; + transition: all 1s ease-in-out; + + & .item { + padding: 2px; + } +} + .disabled { opacity: 0.3; pointer-events: none; diff --git a/pages/txs.vue b/pages/txs.vue index dfb65072..b9ae1997 100644 --- a/pages/txs.vue +++ b/pages/txs.vue @@ -111,19 +111,22 @@ const handleClearAllFilters = () => { const searchTerm = ref("") /** Parse route query */ -Object.keys(route.query).forEach((key) => { - if (key === "page") return - - if (key === "from" || key === "to") { - filters[key] = route.query[key] - } else if (route.query[key].split(",").length) { - route.query[key].split(",").forEach((item) => { - filters[key][item] = true - }) - } else { - filters[key][route.query[key]] = true - } -}) +const parseRouteQuery = () => { + Object.keys(route.query).forEach((key) => { + if (key === "page") return + + if (key === "from" || key === "to") { + filters[key] = route.query[key] + } else if (route.query[key].split(",").length) { + route.query[key].split(",").forEach((item) => { + filters[key][item] = true + }) + } else { + filters[key][route.query[key]] = true + } + }) +} +parseRouteQuery() const updateRouteQuery = () => { router.replace({ @@ -223,10 +226,15 @@ const handleUpdateDateFilter = (event) => { const resetFilters = (target, refetch) => { if (target === "from" || target === "to") { filters[target] = "" - } else { + } else if (target) { Object.keys(filters[target]).forEach((f) => { filters[target][f] = false }) + } else { + resetFilters("from") + resetFilters("to") + resetFilters("status") + resetFilters("message_type") } if (refetch) { @@ -315,6 +323,21 @@ onBeforeMount(() => { }) /** Refetch transactions */ +watch( + () => route.query, + () => { + if (!isRefetching.value) { + if (Object.keys(route.query).length) { + parseRouteQuery() + } else { + resetFilters() + } + + getTransactions() + } + }, +) + watch( () => page.value, async () => { diff --git a/pages/validators.vue b/pages/validators.vue index 43fd51f1..6cfe5bb5 100644 --- a/pages/validators.vue +++ b/pages/validators.vue @@ -190,6 +190,10 @@ watch( router.replace({ query: { status: activeTab.value, page: page.value } }) }, ) + +onMounted(() => { + router.replace({ query: { status: activeTab.value, page: page.value } }) +})