From 6b2f03b1b40bd43baceb400b5bedb17f1b6a56b2 Mon Sep 17 00:00:00 2001 From: Mario Sarcevic Date: Thu, 20 Apr 2023 13:49:44 +0200 Subject: [PATCH 01/14] feat(statistics): Move tick values format to utils + Make values display only months for timespan "all" --- .../stardust/statistics/ChartUtils.tsx | 18 ++++++++++++++++++ .../stardust/statistics/charts/BarChart.tsx | 10 ++++------ .../statistics/charts/StackedBarChart.tsx | 11 ++++------- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/client/src/app/components/stardust/statistics/ChartUtils.tsx b/client/src/app/components/stardust/statistics/ChartUtils.tsx index a0b3e2781..52867bbdf 100644 --- a/client/src/app/components/stardust/statistics/ChartUtils.tsx +++ b/client/src/app/components/stardust/statistics/ChartUtils.tsx @@ -1,8 +1,12 @@ import { INodeInfoBaseToken } from "@iota/iota.js-stardust"; +import { ScaleBand } from "d3-scale"; import moment from "moment"; import React, { useCallback, useEffect, useState } from "react"; import { formatAmount } from "../../../../helpers/stardust/valueFormatHelper"; import { IDistributionEntry } from "../../../../models/api/stardust/chronicle/ITokenDistributionResponse"; +import { TimespanOption } from "./ChartHeader"; + +export const DAY_LABEL_FORMAT = "DD MMM"; export const noDataView = () => (
@@ -144,3 +148,17 @@ export const getSubunitThreshold = (tokenInfo: INodeInfoBaseToken) => ( Math.pow(10, tokenInfo.decimals) : null ); +export const barChartsTickValues = ( + timespan: TimespanOption, + axisBand: ScaleBand +) => ( + timespan === "all" ? + axisBand.domain().filter(d => d.includes("01")) : + ( + timespan === "7" ? + axisBand.domain() : + // every third label + axisBand.domain().filter((_, i) => !(i % 3)) + ) +); + diff --git a/client/src/app/components/stardust/statistics/charts/BarChart.tsx b/client/src/app/components/stardust/statistics/charts/BarChart.tsx index 8790ea574..bf24473ca 100644 --- a/client/src/app/components/stardust/statistics/charts/BarChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/BarChart.tsx @@ -11,7 +11,9 @@ import { ModalData } from "../../../ModalProps"; import ChartHeader, { TimespanOption } from "../ChartHeader"; import ChartTooltip from "../ChartTooltip"; import { + barChartsTickValues, d3FormatSpecifier, + DAY_LABEL_FORMAT, determineGraphLeftPadding, noDataView, useChartWrapperSize, @@ -28,8 +30,6 @@ interface BarChartProps { color: string; } -const DAY_LABEL_FORMAT = "DD MMM"; - const BarChart: React.FC = ({ title, info, data, label, color }) => { const [{ wrapperWidth, wrapperHeight }, setTheRef] = useChartWrapperSize(); const chartWrapperRef = useCallback((chartWrapper: HTMLDivElement) => { @@ -92,10 +92,8 @@ const BarChart: React.FC = ({ title, info, data, label, color }) .on("mouseover", mouseoverHandler) .on("mouseout", mouseoutHandler); - const tickValues = timespan === "7" ? - x.domain() : - // every third label - x.domain().filter((_, i) => !(i % 3)); + const tickValues = barChartsTickValues(timespan, x); + const xAxis = axisLabelRotate( axisBottom(x).tickValues(tickValues) ); diff --git a/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx b/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx index e78d8d896..9a612a8aa 100644 --- a/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx @@ -16,7 +16,9 @@ import { useChartWrapperSize, determineGraphLeftPadding, d3FormatSpecifier, - useTouchMoveEffect + useTouchMoveEffect, + barChartsTickValues, + DAY_LABEL_FORMAT } from "../ChartUtils"; import "./Chart.scss"; @@ -29,8 +31,6 @@ interface StackedBarChartProps { data: { [name: string]: number; time: number }[]; } -const DAY_LABEL_FORMAT = "DD MMM"; - const StackedBarChart: React.FC = ({ title, info, @@ -116,10 +116,7 @@ const StackedBarChart: React.FC = ({ .attr("height", d => y(d[0]) - y(d[1])) .attr("width", x.bandwidth()); - const tickValues = timespan === "7" ? - x.domain() : - // every third label - x.domain().filter((_, i) => !(i % 3)); + const tickValues = barChartsTickValues(timespan, x); const xAxis = axisLabelRotate( axisBottom(x).tickValues(tickValues) ); From 882822609c6357d48f1a09afa827123485e3392d Mon Sep 17 00:00:00 2001 From: Mario Sarcevic Date: Tue, 25 Apr 2023 14:38:35 +0200 Subject: [PATCH 02/14] feat(client): Add zooming capabilities to StackedLineChart (brushing). The brushing works on the X axis. Double click on chart "resets" the zoom to default. --- api/.eslintrc.js | 2 +- client/.eslintrc.js | 2 +- client/package-lock.json | 157 +++++++++++++ client/package.json | 4 + .../stardust/statistics/InfluxChartsTab.tsx | 7 + .../statistics/charts/StackedLineChart.tsx | 215 +++++++++++++----- client/src/app/routes.tsx | 2 +- .../src/helpers/stardust/statisticsUtils.ts | 11 + 8 files changed, 334 insertions(+), 66 deletions(-) diff --git a/api/.eslintrc.js b/api/.eslintrc.js index 0bb369474..d0d2ef1ee 100644 --- a/api/.eslintrc.js +++ b/api/.eslintrc.js @@ -451,7 +451,7 @@ module.exports = { "off" ], "generator-star-spacing": [ - "error" + "error", {"before": false, "after": true} ], "getter-return": [ "off" diff --git a/client/.eslintrc.js b/client/.eslintrc.js index 7f8604475..e3ea6c3ff 100644 --- a/client/.eslintrc.js +++ b/client/.eslintrc.js @@ -463,7 +463,7 @@ module.exports = { "off" ], "generator-star-spacing": [ - "error" + "error", {"before": false, "after": true} ], "getter-return": [ "off" diff --git a/client/package-lock.json b/client/package-lock.json index cb0b5cdd2..167a02490 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -24,11 +24,13 @@ "crypto": "npm:crypto-browserify", "d3-array": "^3.2.1", "d3-axis": "^3.0.0", + "d3-brush": "^3.0.0", "d3-format": "^3.1.0", "d3-scale": "^4.0.2", "d3-selection": "^3.0.0", "d3-shape": "^3.1.0", "d3-time-format": "^4.1.0", + "d3-transition": "^3.0.1", "express": "^4.18.1", "jsonschema": "^1.4.1", "moment": "^2.29.4", @@ -54,11 +56,13 @@ "@types/classnames": "^2.3.1", "@types/d3-array": "^3.0.3", "@types/d3-axis": "^3.0.1", + "@types/d3-brush": "^3.0.2", "@types/d3-format": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-selection": "^3.0.3", "@types/d3-shape": "^3.1.0", "@types/d3-time-format": "^4.0.0", + "@types/d3-transition": "^3.0.3", "@types/express": "^4.17.14", "@types/jest": "^29.0.0", "@types/node": "^16.10.3", @@ -4313,6 +4317,15 @@ "@types/d3-selection": "*" } }, + "node_modules/@types/d3-brush": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.2.tgz", + "integrity": "sha512-2TEm8KzUG3N7z0TrSKPmbxByBx54M+S9lHoP2J55QuLU0VSQ9mE96EJSAOVNEqd1bbynMjeTS9VHmz8/bSw8rA==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, "node_modules/@types/d3-format": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", @@ -4361,6 +4374,15 @@ "integrity": "sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==", "dev": true }, + "node_modules/@types/d3-transition": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.3.tgz", + "integrity": "sha512-/S90Od8Id1wgQNvIA8iFv9jRhCiZcGhPd2qX0bKF/PS+y0W5CrXKgIiELd2CvG1mlQrWK/qlYh3VxicqG1ZvgA==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, "node_modules/@types/eslint": { "version": "8.4.6", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.6.tgz", @@ -8016,6 +8038,21 @@ "node": ">=12" } }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-color": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", @@ -8024,6 +8061,34 @@ "node": ">=12" } }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-format": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", @@ -8107,6 +8172,32 @@ "node": ">=12" } }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -24657,6 +24748,15 @@ "@types/d3-selection": "*" } }, + "@types/d3-brush": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.2.tgz", + "integrity": "sha512-2TEm8KzUG3N7z0TrSKPmbxByBx54M+S9lHoP2J55QuLU0VSQ9mE96EJSAOVNEqd1bbynMjeTS9VHmz8/bSw8rA==", + "dev": true, + "requires": { + "@types/d3-selection": "*" + } + }, "@types/d3-format": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", @@ -24705,6 +24805,15 @@ "integrity": "sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==", "dev": true }, + "@types/d3-transition": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.3.tgz", + "integrity": "sha512-/S90Od8Id1wgQNvIA8iFv9jRhCiZcGhPd2qX0bKF/PS+y0W5CrXKgIiELd2CvG1mlQrWK/qlYh3VxicqG1ZvgA==", + "dev": true, + "requires": { + "@types/d3-selection": "*" + } + }, "@types/eslint": { "version": "8.4.6", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.6.tgz", @@ -27489,11 +27598,42 @@ "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==" }, + "d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + } + }, "d3-color": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" }, + "d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==" + }, + "d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + } + }, + "d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" + }, "d3-format": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", @@ -27553,6 +27693,23 @@ "d3-time": "1 - 3" } }, + "d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==" + }, + "d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "requires": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + } + }, "damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", diff --git a/client/package.json b/client/package.json index 1b57fd8b6..62c2d0a55 100644 --- a/client/package.json +++ b/client/package.json @@ -32,11 +32,13 @@ "crypto": "npm:crypto-browserify", "d3-array": "^3.2.1", "d3-axis": "^3.0.0", + "d3-brush": "^3.0.0", "d3-format": "^3.1.0", "d3-scale": "^4.0.2", "d3-selection": "^3.0.0", "d3-shape": "^3.1.0", "d3-time-format": "^4.1.0", + "d3-transition": "^3.0.1", "express": "^4.18.1", "jsonschema": "^1.4.1", "moment": "^2.29.4", @@ -62,11 +64,13 @@ "@types/classnames": "^2.3.1", "@types/d3-array": "^3.0.3", "@types/d3-axis": "^3.0.1", + "@types/d3-brush": "^3.0.2", "@types/d3-format": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-selection": "^3.0.3", "@types/d3-shape": "^3.1.0", "@types/d3-time-format": "^4.0.0", + "@types/d3-transition": "^3.0.3", "@types/express": "^4.17.14", "@types/jest": "^29.0.0", "@types/node": "^16.10.3", diff --git a/client/src/app/components/stardust/statistics/InfluxChartsTab.tsx b/client/src/app/components/stardust/statistics/InfluxChartsTab.tsx index 69a27b82d..3535a23e2 100644 --- a/client/src/app/components/stardust/statistics/InfluxChartsTab.tsx +++ b/client/src/app/components/stardust/statistics/InfluxChartsTab.tsx @@ -1,6 +1,7 @@ import React, { useContext } from "react"; import graphMessages from "../../../../assets/modals/stardust/statistics/graphs.json"; import { useChartsState } from "../../../../helpers/hooks/useChartsState"; +import { idGenerator } from "../../../../helpers/stardust/statisticsUtils"; import { formatAmount } from "../../../../helpers/stardust/valueFormatHelper"; import NetworkContext from "../../../context/NetworkContext"; import { COMMAS_REGEX } from "../../../routes/stardust/landing/ShimmerClaimedUtils"; @@ -37,6 +38,8 @@ export const InfluxChartsTab: React.FC = () => { tokenInfo ).replace(COMMAS_REGEX, ",") ?? "-"; + const ids = idGenerator(); + return (
@@ -70,6 +73,7 @@ export const InfluxChartsTab: React.FC = () => {
{ data={outputs} /> {
{
= ({ + chartId, title, info, subgroups, @@ -60,9 +62,9 @@ const StackedLineChart: React.FC = ({ data = timespan !== "all" ? data.slice(-timespan) : data; - const dataMaxY = Math.max(...data.map(d => Math.max(...subgroups.map(key => d[key])))); - const leftMargin = determineGraphLeftPadding(dataMaxY); - + // chart dimensions + const yMax = Math.max(...data.map(d => Math.max(...subgroups.map(key => d[key])))); + const leftMargin = determineGraphLeftPadding(yMax); const MARGIN = { top: 30, right: 20, bottom: 50, left: leftMargin }; const INNER_WIDTH = width - MARGIN.left - MARGIN.right; const INNER_HEIGHT = height - MARGIN.top - MARGIN.bottom; @@ -80,42 +82,55 @@ const StackedLineChart: React.FC = ({ d => timestampToDate(d.time) ); + // SVG const svg = select(theSvg.current) .attr("viewBox", `0 0 ${width} ${height}`) .attr("preserveAspectRatio", "none") .append("g") .attr("transform", `translate(${MARGIN.left}, ${MARGIN.top})`); + // X const x = scaleTime() .domain([groups[0], groups[groups.length - 1]]) .range([0, INNER_WIDTH]); + const xAxis = axisBottom(x); - const y = scaleLinear().domain([0, dataMaxY]) - .range([INNER_HEIGHT, 0]); + const xAxisSelection = svg.append("g") + .attr("class", "axis axis--x") + .attr("transform", `translate(0, ${INNER_HEIGHT})`) + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + .call(xAxis); - const yAxisGrid = axisLeft(y.nice()).tickFormat(format(d3FormatSpecifier(dataMaxY))); + // Y + const y = scaleLinear().domain([0, yMax]) + .range([INNER_HEIGHT, 0]); + const yAxisGrid = axisLeft(y.nice()).tickFormat(format(d3FormatSpecifier(yMax))); svg.append("g") .attr("class", "axis axis--y") .call(yAxisGrid); - const xAxis = axisLabelRotate( - axisBottom(x).tickFormat(timeFormat("%d %b")) - ); - - svg.append("g") - .attr("class", "axis axis--x") - .attr("transform", `translate(0, ${INNER_HEIGHT})`) - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - .call(xAxis); + // clap path + svg.append("defs") + .append("clipPath") + .attr("id", `clip-${chartId}`) + .append("rect") + .attr("width", width) + .attr("height", height) + .attr("x", 0) + .attr("y", 0); + // area fill const areaGen = area>() .x(d => x(timestampToDate(d.data.time)) ?? 0) .y0(_ => y(0)) .y1(d => y(d[1] - d[0])); - svg.append("g") - .selectAll("g") + const theArea = svg.append("g") + .attr("class", "areas") + .attr("clip-path", `url(#clip-${chartId})`); + + const areaSelection = theArea.selectAll("g") .data(stackedData) .join("path") .style("fill", d => getGradient(d.key, color(d.key))) @@ -123,63 +138,137 @@ const StackedLineChart: React.FC = ({ .attr("class", "area") .attr("d", areaGen); + // area lines path const lineGen = line>() .x(d => x(timestampToDate(d.data.time)) ?? 0) .y(d => y(d[1] - d[0])); - svg.append("g") + const lineSelection = svg.append("g") + .attr("class", "lines") + .attr("clip-path", `url(#clip-${chartId})`) .selectAll("g") .data(stackedData) .join("path") .attr("fill", "none") .attr("stroke", d => color(d.key)) .attr("stroke-width", 2) + .attr("class", "line") .attr("d", lineGen); - for (const dataStack of stackedData) { + const attachOnHoverLinesAndCircles = () => { + svg.selectAll(".hover-circles").remove(); + svg.selectAll(".hover-lines").remove(); + const halfLineWidth = data.length > 1 ? + ((x(timestampToDate(data[1].time)) ?? 0) - (x(timestampToDate(data[0].time)) ?? 0)) / 2 : + 18; + + for (const dataStack of stackedData) { + svg.append("g") + .attr("class", "hover-circles") + .attr("clip-path", `url(#clip-${chartId})`) + .selectAll("g") + .data(dataStack) + .enter() + .append("circle") + .attr("fill", color(dataStack.key)) + .style("stroke", color(dataStack.key)) + .style("stroke-width", 5) + .style("stroke-opacity", 0) + .attr("cx", d => x(timestampToDate(d.data.time)) ?? 0) + .attr("cy", d => y(d[1] - d[0])) + .attr("r", 0) + .attr("class", (_, i) => `circle-${i}`); + } + + // hover lines for tooltip svg.append("g") + .attr("class", "hover-lines") + .attr("clip-path", `url(#clip-${chartId})`) .selectAll("g") - .data(dataStack) + .data(data) .enter() - .append("circle") - .attr("fill", color(dataStack.key)) - .style("stroke", color(dataStack.key)) - .style("stroke-width", 5) - .style("stroke-opacity", 0) - .attr("cx", d => x(timestampToDate(d.data.time)) ?? 0) - .attr("cy", d => y(d[1] - d[0])) - .attr("r", 1) - .attr("class", (_, i) => `circle-${i}`); - } - - const halfLineWidth = data.length > 1 ? - ((x(timestampToDate(data[1].time)) ?? 0) - (x(timestampToDate(data[0].time)) ?? 0)) / 2 : - 18; - - svg.append("g") - .attr("class", "hover-lines") - .selectAll("g") - .data(data) - .enter() - .append("rect") - .attr("fill", "transparent") - .attr("x", (_, idx) => ( - idx === 0 ? 0 : (x(timestampToDate(data[idx].time)) ?? 0) - halfLineWidth - )) - .attr("y", 0) - .attr("class", (_, i) => `rect-${i}`) - .attr("height", INNER_HEIGHT) - .attr("width", (_, idx) => { - if (idx === 0) { - return halfLineWidth; - } else if (idx === data.length - 1) { - return halfLineWidth; + .append("rect") + .attr("fill", "transparent") + .attr("x", (_, idx) => ( + idx === 0 ? 0 : (x(timestampToDate(data[idx].time)) ?? 0) - halfLineWidth + )) + .attr("y", 0) + .attr("class", (_, i) => `rect-${i}`) + .attr("height", INNER_HEIGHT) + .attr("width", (_, idx) => ( + (idx === 0 || idx === data.length - 1) ? + halfLineWidth : halfLineWidth * 2 + )) + .on("mouseover", mouseoverHandler) + .on("mouseout", mouseoutHandler); + }; + + // brushing + const brush = brushX() + .extent([[0, 0], [INNER_WIDTH, height]]) + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + .on("end", e => onBrushHandler(e)); + + const brushSelection = theArea.append("g") + .attr("class", "brush") + .call(brush); + + let idleTimeout: NodeJS.Timer | null = null; + const idled = () => { + idleTimeout = null; + }; + const onBrushHandler = (event: D3BrushEvent<{ [key: string]: number }>) => { + if (!event.selection) { + return; + } + const extent = event.selection; + if (!extent) { + if (!idleTimeout) { + idleTimeout = setTimeout(idled, 350); + return idleTimeout; } - - return halfLineWidth * 2; - }) - .on("mouseover", mouseoverHandler) - .on("mouseout", mouseoutHandler); + x.domain([groups[0], groups[groups.length - 1]]); + } else { + console.log(extent); + console.log(x.invert(extent[0] as NumberValue)); + console.log(x.invert(extent[1] as NumberValue)); + x.domain([x.invert(extent[0] as NumberValue), x.invert(extent[1] as NumberValue)]); + // eslint-disable-next-line @typescript-eslint/unbound-method + brushSelection.call(brush.move, null); + } + + // Update axis, area and lines position + xAxisSelection.transition().duration(1000).call(axisBottom(x)); + areaSelection + .transition() + .duration(750) + .attr("d", areaGen); + lineSelection + .transition() + .duration(750) + .attr("d", lineGen); + + // rebuild the hover activated lines & cicles + attachOnHoverLinesAndCircles(); + }; + + // double click reset + svg.on("dblclick", () => { + x.domain([groups[0], groups[groups.length - 1]]); + xAxisSelection.transition().call(axisBottom(x)); + areaSelection + .transition() + .duration(500) + .attr("d", areaGen); + lineSelection + .transition() + .duration(500) + .attr("d", lineGen); + + attachOnHoverLinesAndCircles(); + }); + + attachOnHoverLinesAndCircles(); } }, [data, timespan, wrapperWidth, wrapperHeight]); @@ -258,7 +347,7 @@ const StackedLineChart: React.FC = ({ select(theSvg.current) .selectAll(`.circle-${idx}`) - .attr("r", 1) + .attr("r", 0) .style("stroke-opacity", 0); activeElement diff --git a/client/src/app/routes.tsx b/client/src/app/routes.tsx index 765215e43..c3ac14a0a 100644 --- a/client/src/app/routes.tsx +++ b/client/src/app/routes.tsx @@ -47,7 +47,7 @@ import { VisualizerRouteProps } from "./routes/VisualizerRouteProps"; * @yields The next value. * @returns The iterator. */ -function *keyGenerator(count: number): IterableIterator { +function* keyGenerator(count: number): IterableIterator { while (true) { yield count++; } diff --git a/client/src/helpers/stardust/statisticsUtils.ts b/client/src/helpers/stardust/statisticsUtils.ts index c61063ef8..b53eb2486 100644 --- a/client/src/helpers/stardust/statisticsUtils.ts +++ b/client/src/helpers/stardust/statisticsUtils.ts @@ -114,3 +114,14 @@ export function mapDailyStatsToGraphsData(data: IInfluxDailyResponse): IStatisti }; } +/** + * Generator function to yield incrementing string ids. + * @yields next id. + */ +export function* idGenerator(): Generator { + let id: number = 0; + while (true) { + yield (id++).toString(); + } +} + From d9b12beb33afce4a0d7f1a44059c4560a6abe03b Mon Sep 17 00:00:00 2001 From: Mario Sarcevic Date: Tue, 25 Apr 2023 14:40:19 +0200 Subject: [PATCH 03/14] chore(api): Lower the refresh interval of currecnyService to "every 5 mins" --- api/src/initServices.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/initServices.ts b/api/src/initServices.ts index 6cf9f0d0d..181ab18f1 100644 --- a/api/src/initServices.ts +++ b/api/src/initServices.ts @@ -96,7 +96,7 @@ export async function initServices(config: IConfiguration) { void currencyService.update(); }; - setInterval(update, 60000); + setInterval(update, 5 * 60000); await update(); } From f346576afa6e32dc866a1abeb00371614107de2e Mon Sep 17 00:00:00 2001 From: Mario Sarcevic Date: Wed, 26 Apr 2023 17:54:52 +0200 Subject: [PATCH 04/14] feat(client): Add zooming capabilities to StackedBarChart (brushing) --- client/package-lock.json | 14 +- client/package.json | 2 + .../stardust/statistics/ChartUtils.tsx | 26 ++- .../stardust/statistics/InfluxChartsTab.tsx | 11 +- .../stardust/statistics/charts/Chart.scss | 9 +- .../statistics/charts/StackedBarChart.tsx | 155 +++++++++++++----- .../statistics/charts/StackedLineChart.tsx | 27 ++- 7 files changed, 175 insertions(+), 69 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 167a02490..b17d92592 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -29,6 +29,7 @@ "d3-scale": "^4.0.2", "d3-selection": "^3.0.0", "d3-shape": "^3.1.0", + "d3-time": "^3.1.0", "d3-time-format": "^4.1.0", "d3-transition": "^3.0.1", "express": "^4.18.1", @@ -61,6 +62,7 @@ "@types/d3-scale": "^4.0.2", "@types/d3-selection": "^3.0.3", "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", "@types/d3-time-format": "^4.0.0", "@types/d3-transition": "^3.0.3", "@types/express": "^4.17.14", @@ -8151,9 +8153,9 @@ } }, "node_modules/d3-time": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.0.0.tgz", - "integrity": "sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", "dependencies": { "d3-array": "2 - 3" }, @@ -27678,9 +27680,9 @@ } }, "d3-time": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.0.0.tgz", - "integrity": "sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", "requires": { "d3-array": "2 - 3" } diff --git a/client/package.json b/client/package.json index 62c2d0a55..aca5cb1a6 100644 --- a/client/package.json +++ b/client/package.json @@ -37,6 +37,7 @@ "d3-scale": "^4.0.2", "d3-selection": "^3.0.0", "d3-shape": "^3.1.0", + "d3-time": "^3.1.0", "d3-time-format": "^4.1.0", "d3-transition": "^3.0.1", "express": "^4.18.1", @@ -69,6 +70,7 @@ "@types/d3-scale": "^4.0.2", "@types/d3-selection": "^3.0.3", "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", "@types/d3-time-format": "^4.0.0", "@types/d3-transition": "^3.0.3", "@types/express": "^4.17.14", diff --git a/client/src/app/components/stardust/statistics/ChartUtils.tsx b/client/src/app/components/stardust/statistics/ChartUtils.tsx index 52867bbdf..0aa214823 100644 --- a/client/src/app/components/stardust/statistics/ChartUtils.tsx +++ b/client/src/app/components/stardust/statistics/ChartUtils.tsx @@ -1,5 +1,7 @@ import { INodeInfoBaseToken } from "@iota/iota.js-stardust"; -import { ScaleBand } from "d3-scale"; +import { NumberValue, ScaleBand } from "d3-scale"; +import { timeDay, timeMonth, timeWeek, timeYear } from "d3-time"; +import { timeFormat } from "d3-time-format"; import moment from "moment"; import React, { useCallback, useEffect, useState } from "react"; import { formatAmount } from "../../../../helpers/stardust/valueFormatHelper"; @@ -162,3 +164,25 @@ export const barChartsTickValues = ( ) ); +const formatHidden = timeFormat(""); +const formatDay = timeFormat("%a %d"); +const formatWeek = timeFormat("%b %d"); +const formatMonth = timeFormat("%B"); +const formatYear = timeFormat("%Y"); + +export const tickMultiFormat = (date: Date | NumberValue) => { + const theDate = date as Date; + if (timeDay(theDate) < theDate) { + return formatHidden(theDate); + } else if (timeMonth(theDate) < theDate) { + if (timeWeek(theDate) < theDate) { + return formatDay(theDate); + } + return formatWeek(theDate); + } else if (timeYear(theDate) < theDate) { + return formatMonth(theDate); + } + + return formatYear(theDate); +}; + diff --git a/client/src/app/components/stardust/statistics/InfluxChartsTab.tsx b/client/src/app/components/stardust/statistics/InfluxChartsTab.tsx index 3535a23e2..229c47c38 100644 --- a/client/src/app/components/stardust/statistics/InfluxChartsTab.tsx +++ b/client/src/app/components/stardust/statistics/InfluxChartsTab.tsx @@ -48,15 +48,17 @@ export const InfluxChartsTab: React.FC = () => {
- - {
{ data={aliasActivity} /> {
= ({ + chartId, title, info, subgroups, @@ -61,7 +62,8 @@ const StackedBarChart: React.FC = ({ data = timespan !== "all" ? data.slice(-timespan) : data; - const dataMaxY = Math.max( + // chart dimensions + const yMax = Math.max( ...data.map(d => { let sum = 0; for (const key of subgroups) { @@ -70,62 +72,133 @@ const StackedBarChart: React.FC = ({ return sum; }) ); - const leftMargin = determineGraphLeftPadding(dataMaxY); - + const leftMargin = determineGraphLeftPadding(yMax); const MARGIN = { top: 30, right: 20, bottom: 50, left: leftMargin }; const INNER_WIDTH = width - MARGIN.left - MARGIN.right; const INNER_HEIGHT = height - MARGIN.top - MARGIN.bottom; const color = scaleOrdinal().domain(subgroups).range(colors); - const groups = data.map(d => moment.unix(d.time).format(DAY_LABEL_FORMAT)); - const x = scaleBand().domain(groups) - .range([0, INNER_WIDTH]) - .paddingInner(0.1); + const timestampToDate = (timestamp: number) => moment.unix(timestamp) + .hours(0) + .minutes(0) + .toDate(); - const y = scaleLinear().domain([0, dataMaxY]) - .range([INNER_HEIGHT, 0]); + const groups = data.map( + d => timestampToDate(d.time) + ); + const stackedData = stack().keys(subgroups)(data); + // SVG const svg = select(theSvg.current) .attr("viewBox", `0 0 ${width} ${height}`) .attr("preserveAspectRatio", "none") .append("g") .attr("transform", `translate(${MARGIN.left}, ${MARGIN.top})`); - const yAxisGrid = axisLeft(y.nice()).tickFormat(format(d3FormatSpecifier(dataMaxY))); + // X + const x = scaleTime() + .domain([groups[0], groups[groups.length - 1]]) + .range([0, INNER_WIDTH]); + const xAxis = axisBottom(x).tickFormat(tickMultiFormat); + + const xAxisSelection = svg.append("g") + .attr("class", "axis axis--x") + .attr("transform", `translate(0, ${INNER_HEIGHT})`) + .call(xAxis); + + // Y + const y = scaleLinear().domain([0, yMax]) + .range([INNER_HEIGHT, 0]); + const yAxisGrid = axisLeft(y.nice()).tickFormat(format(d3FormatSpecifier(yMax))); svg.append("g") .attr("class", "axis axis--y") .call(yAxisGrid); - const stackedData = stack().keys(subgroups)(data); + // clip path + svg.append("defs") + .append("clipPath") + .attr("id", `clip-${chartId}`) + .append("rect") + .attr("width", INNER_WIDTH) + .attr("height", height) + .attr("x", 0) + .attr("y", 0); - svg.append("g") - .selectAll("g") - .data(stackedData) - .join("g") - .attr("fill", d => color(d.key)) - .selectAll("rect") - .data(d => d) - .join("rect") - .attr("x", d => x(moment.unix(d.data.time).format(DAY_LABEL_FORMAT)) ?? 0) - .attr("y", d => y(d[1])) - .attr("class", (_, i) => `stacked-bar rect-${i}`) - .on("mouseover", mouseoverHandler) - .on("mouseout", mouseoutHandler) - .attr("height", d => y(d[0]) - y(d[1])) - .attr("width", x.bandwidth()); - - const tickValues = barChartsTickValues(timespan, x); - const xAxis = axisLabelRotate( - axisBottom(x).tickValues(tickValues) - ); + // brushing + const brush = brushX() + .extent([[0, 0], [INNER_WIDTH, height]]) + .on("end", e => onBrushHandler(e as D3BrushEvent<{ [key: string]: number }>)); - svg.append("g") - .attr("class", "axis axis--x") - .attr("transform", `translate(0, ${INNER_HEIGHT})`) - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - .call(xAxis); + const brushSelection = svg.append("g") + .attr("class", "brush") + .call(brush); + + // bars + const barsSelection = svg.append("g") + .attr("class", "stacked-bars") + .attr("clip-path", `url(#clip-${chartId})`); + + const renderBars = (datesLen: number) => { + barsSelection.selectAll("g") + .data(stackedData) + .join("g") + .attr("fill", d => color(d.key)) + .selectAll("rect") + .data(d => d) + .join("rect") + .attr("x", d => x(timestampToDate(d.data.time)) - ((INNER_WIDTH / datesLen) / 2)) + .attr("y", d => y(d[1])) + .attr("class", (_, i) => `stacked-bar rect-${i}`) + .on("mouseover", mouseoverHandler) + .on("mouseout", mouseoutHandler) + .attr("height", d => y(d[0]) - y(d[1])) + .attr("width", INNER_WIDTH / datesLen); + }; + + renderBars(data.length); + + const onBrushHandler = (event: D3BrushEvent<{ [key: string]: number }>) => { + if (!event.selection) { + return; + } + const extent = event.selection; + if (!extent) { + x.domain([groups[0], groups[groups.length - 1]]); + } else { + console.log(extent); + x.domain([x.invert(extent[0] as NumberValue), x.invert(extent[1] as NumberValue)]); + // eslint-disable-next-line @typescript-eslint/unbound-method + brushSelection.call(brush.move, null); + } + + // compute bars count included in barsSelection + const from = x.domain()[0]; + from.setHours(0, 0, 0, 0); + const to = x.domain()[1]; + to.setHours(0, 0, 0, 0); + let barsCount = 0; + for (const d of data) { + const target = timestampToDate(d.time); + target.setHours(0, 0, 0, 0); + if (from <= target && target <= to) { + barsCount++; + } + } + + // Update bars + renderBars(barsCount); + // Update axis, area and lines position + xAxisSelection.transition().duration(1000).call(axisBottom(x).tickFormat(tickMultiFormat)); + }; + + // double click reset + svg.on("dblclick", () => { + x.domain([groups[0], groups[groups.length - 1]]); + xAxisSelection.transition().call(axisBottom(x).tickFormat(tickMultiFormat)); + renderBars(data.length); + }); } }, [data, timespan, wrapperWidth, wrapperHeight]); diff --git a/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx b/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx index a72405a81..02bb6bede 100644 --- a/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx @@ -1,9 +1,8 @@ -// import { axisBottom, axisLabelRotate } from "@d3fc/d3fc-axis"; import classNames from "classnames"; -import { axisBottom, axisLeft } from "d3-axis"; +import { Axis, axisBottom, axisLeft } from "d3-axis"; import { brushX, D3BrushEvent } from "d3-brush"; import { format } from "d3-format"; -import { scaleTime, scaleLinear, scaleOrdinal, NumberValue } from "d3-scale"; +import { scaleTime, scaleLinear, scaleOrdinal, NumberValue, ScaleTime } from "d3-scale"; import { BaseType, select } from "d3-selection"; import { area, line, SeriesPoint, stack } from "d3-shape"; import moment from "moment"; @@ -15,6 +14,7 @@ import { d3FormatSpecifier, determineGraphLeftPadding, noDataView, + tickMultiFormat, useChartWrapperSize, useMultiValueTooltip, useTouchMoveEffect @@ -93,13 +93,14 @@ const StackedLineChart: React.FC = ({ const x = scaleTime() .domain([groups[0], groups[groups.length - 1]]) .range([0, INNER_WIDTH]); - const xAxis = axisBottom(x); + + const buildXAxis: (scale: ScaleTime) => Axis = scale => + axisBottom(scale).tickFormat(tickMultiFormat) as Axis; const xAxisSelection = svg.append("g") .attr("class", "axis axis--x") .attr("transform", `translate(0, ${INNER_HEIGHT})`) - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - .call(xAxis); + .call(buildXAxis(x)); // Y const y = scaleLinear().domain([0, yMax]) @@ -110,7 +111,7 @@ const StackedLineChart: React.FC = ({ .attr("class", "axis axis--y") .call(yAxisGrid); - // clap path + // clip path svg.append("defs") .append("clipPath") .attr("id", `clip-${chartId}`) @@ -206,10 +207,9 @@ const StackedLineChart: React.FC = ({ // brushing const brush = brushX() .extent([[0, 0], [INNER_WIDTH, height]]) - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - .on("end", e => onBrushHandler(e)); + .on("end", e => onBrushHandler(e as D3BrushEvent<{ [key: string]: number }>)); - const brushSelection = theArea.append("g") + const brushSelection = svg.append("g") .attr("class", "brush") .call(brush); @@ -229,16 +229,13 @@ const StackedLineChart: React.FC = ({ } x.domain([groups[0], groups[groups.length - 1]]); } else { - console.log(extent); - console.log(x.invert(extent[0] as NumberValue)); - console.log(x.invert(extent[1] as NumberValue)); x.domain([x.invert(extent[0] as NumberValue), x.invert(extent[1] as NumberValue)]); // eslint-disable-next-line @typescript-eslint/unbound-method brushSelection.call(brush.move, null); } // Update axis, area and lines position - xAxisSelection.transition().duration(1000).call(axisBottom(x)); + xAxisSelection.transition().duration(1000).call(buildXAxis(x)); areaSelection .transition() .duration(750) @@ -255,7 +252,7 @@ const StackedLineChart: React.FC = ({ // double click reset svg.on("dblclick", () => { x.domain([groups[0], groups[groups.length - 1]]); - xAxisSelection.transition().call(axisBottom(x)); + xAxisSelection.transition().call(buildXAxis(x)); areaSelection .transition() .duration(500) From 36d495452a89da03bc6664e5c45b66153b837f53 Mon Sep 17 00:00:00 2001 From: Mario Sarcevic Date: Wed, 26 Apr 2023 19:57:00 +0200 Subject: [PATCH 05/14] feat(client): Add brushing to LineChart and BarChart --- .../stardust/statistics/ChartUtils.tsx | 5 + .../stardust/statistics/InfluxChartsTab.tsx | 6 + .../stardust/statistics/charts/BarChart.tsx | 148 ++++++++---- .../stardust/statistics/charts/LineChart.tsx | 219 +++++++++++------- .../statistics/charts/StackedBarChart.tsx | 10 +- .../statistics/charts/StackedLineChart.tsx | 12 +- 6 files changed, 262 insertions(+), 138 deletions(-) diff --git a/client/src/app/components/stardust/statistics/ChartUtils.tsx b/client/src/app/components/stardust/statistics/ChartUtils.tsx index 0aa214823..dd05d1b66 100644 --- a/client/src/app/components/stardust/statistics/ChartUtils.tsx +++ b/client/src/app/components/stardust/statistics/ChartUtils.tsx @@ -186,3 +186,8 @@ export const tickMultiFormat = (date: Date | NumberValue) => { return formatYear(theDate); }; +export const timestampToDate = (timestamp: number) => moment.unix(timestamp) + .hours(0) + .minutes(0) + .toDate(); + diff --git a/client/src/app/components/stardust/statistics/InfluxChartsTab.tsx b/client/src/app/components/stardust/statistics/InfluxChartsTab.tsx index 229c47c38..174ccf09d 100644 --- a/client/src/app/components/stardust/statistics/InfluxChartsTab.tsx +++ b/client/src/app/components/stardust/statistics/InfluxChartsTab.tsx @@ -115,6 +115,7 @@ export const InfluxChartsTab: React.FC = () => {
{ data={addressesWithBalance} /> {
{
{ data={unclaimedTokens} /> { data={ledgerSize} /> = ({ title, info, data, label, color }) => { +const BarChart: React.FC = ({ chartId, title, info, data, label, color }) => { const [{ wrapperWidth, wrapperHeight }, setTheRef] = useChartWrapperSize(); const chartWrapperRef = useCallback((chartWrapper: HTMLDivElement) => { if (chartWrapper !== null) { @@ -53,56 +53,124 @@ const BarChart: React.FC = ({ title, info, data, label, color }) data = timespan !== "all" ? data.slice(-timespan) : data; - const dataMaxY = max(data, d => d.n) ?? 1; - const leftMargin = determineGraphLeftPadding(dataMaxY); - + // chart dimensions + const yMax = max(data, d => d.n) ?? 1; + const leftMargin = determineGraphLeftPadding(yMax); const MARGIN = { top: 30, right: 20, bottom: 50, left: leftMargin }; const INNER_WIDTH = width - MARGIN.left - MARGIN.right; const INNER_HEIGHT = height - MARGIN.top - MARGIN.bottom; - const x = scaleBand().domain(data.map(d => moment.unix(d.time).format(DAY_LABEL_FORMAT))) - .range([0, INNER_WIDTH]) - .paddingInner(0.1); - - const y = scaleLinear().domain([0, dataMaxY]) - .range([INNER_HEIGHT, 0]); + const dates = data.map(d => timestampToDate(d.time)); + // SVG const svg = select(theSvg.current) .attr("viewBox", `0 0 ${width} ${height}`) .attr("preserveAspectRatio", "none") .append("g") .attr("transform", `translate(${MARGIN.left}, ${MARGIN.top})`); - const yAxisGrid = axisLeft(y.nice()).tickFormat(format(d3FormatSpecifier(dataMaxY))); + // X + const x = scaleTime() + .domain([dates[0], dates[dates.length - 1]]) + .range([0, INNER_WIDTH]); + + const buildXAxis: (scale: ScaleTime) => Axis = scale => + axisBottom(scale).tickFormat(tickMultiFormat) as Axis; + + const xAxisSelection = svg.append("g") + .attr("class", "axis axis--x") + .attr("transform", `translate(0, ${INNER_HEIGHT})`) + .call(buildXAxis(x)); + + // Y + const y = scaleLinear().domain([0, yMax]) + .range([INNER_HEIGHT, 0]); + const yAxisGrid = axisLeft(y.nice()).tickFormat(format(d3FormatSpecifier(yMax))); svg.append("g") .attr("class", "axis axis--y") .call(yAxisGrid); - svg.selectAll(".bar") - .data(data) - .enter() + // clip path + svg.append("defs") + .append("clipPath") + .attr("id", `clip-${chartId}`) .append("rect") - .attr("class", "bar") - .attr("x", d => x(moment.unix(d.time).format(DAY_LABEL_FORMAT)) ?? 0) - .attr("width", x.bandwidth()) - .attr("y", d => y(d.n)) - .attr("height", d => INNER_HEIGHT - y(d.n)) - .attr("fill", color) - .on("mouseover", mouseoverHandler) - .on("mouseout", mouseoutHandler); - - const tickValues = barChartsTickValues(timespan, x); - - const xAxis = axisLabelRotate( - axisBottom(x).tickValues(tickValues) - ); - - svg.append("g") - .attr("class", "axis axis--x") - .attr("transform", `translate(0, ${INNER_HEIGHT})`) - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - .call(xAxis); + .attr("width", INNER_WIDTH) + .attr("height", height) + .attr("x", 0) + .attr("y", 0); + + // brushing + const brush = brushX() + .extent([[0, 0], [INNER_WIDTH, height]]) + .on("end", e => onBrushHandler(e as D3BrushEvent<{ [key: string]: number }>)); + + const brushSelection = svg.append("g") + .attr("class", "brush") + .call(brush); + + // bars + const renderBars = (datesLen: number) => { + svg.selectAll(".the-bars").remove(); + svg.append("g") + .attr("class", "the-bars") + .attr("clip-path", `url(#clip-${chartId})`) + .selectAll("g") + .data(data) + .enter() + .append("rect") + .attr("class", "bar") + .attr("x", d => x(timestampToDate(d.time)) - ((INNER_WIDTH / datesLen) / 2)) + .attr("y", d => y(d.n)) + .attr("fill", color) + .on("mouseover", mouseoverHandler) + .on("mouseout", mouseoutHandler) + .attr("width", INNER_WIDTH / datesLen) + .attr("height", d => INNER_HEIGHT - y(d.n)); + }; + + renderBars(data.length); + + const onBrushHandler = (event: D3BrushEvent<{ [key: string]: number }>) => { + if (!event.selection) { + return; + } + const extent = event.selection; + if (!extent) { + x.domain([dates[0], dates[dates.length - 1]]); + } else { + x.domain([x.invert(extent[0] as NumberValue), x.invert(extent[1] as NumberValue)]); + // eslint-disable-next-line @typescript-eslint/unbound-method + brushSelection.call(brush.move, null); + } + + // compute bars count included in barsSelection + const from = x.domain()[0]; + from.setHours(0, 0, 0, 0); + const to = x.domain()[1]; + to.setHours(0, 0, 0, 0); + let barsCount = 0; + for (const d of data) { + const target = timestampToDate(d.time); + target.setHours(0, 0, 0, 0); + if (from <= target && target <= to) { + barsCount++; + } + } + + // Update bars + renderBars(barsCount); + // Update axis, area and lines position + xAxisSelection.transition().duration(1000).call(axisBottom(x).tickFormat(tickMultiFormat)); + }; + + // double click reset + svg.on("dblclick", () => { + x.domain([dates[0], dates[dates.length - 1]]); + xAxisSelection.transition().call(axisBottom(x).tickFormat(tickMultiFormat)); + renderBars(data.length); + }); } }, [data, timespan, wrapperWidth, wrapperHeight]); diff --git a/client/src/app/components/stardust/statistics/charts/LineChart.tsx b/client/src/app/components/stardust/statistics/charts/LineChart.tsx index ae6c622ce..7899b4b29 100644 --- a/client/src/app/components/stardust/statistics/charts/LineChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/LineChart.tsx @@ -1,13 +1,11 @@ -import { axisBottom, axisLabelRotate } from "@d3fc/d3fc-axis"; import classNames from "classnames"; import { max } from "d3-array"; -import { axisLeft } from "d3-axis"; +import { Axis, axisBottom, axisLeft } from "d3-axis"; +import { brushX, D3BrushEvent } from "d3-brush"; import { format } from "d3-format"; -import { scaleLinear, scaleTime } from "d3-scale"; +import { NumberValue, scaleLinear, ScaleTime, scaleTime } from "d3-scale"; import { BaseType, select } from "d3-selection"; import { line } from "d3-shape"; -import { timeFormat } from "d3-time-format"; -import moment from "moment"; import React, { useCallback, useLayoutEffect, useRef, useState } from "react"; import { ModalData } from "../../../ModalProps"; import ChartHeader, { TimespanOption } from "../ChartHeader"; @@ -16,6 +14,8 @@ import { d3FormatSpecifier, determineGraphLeftPadding, noDataView, + tickMultiFormat, + timestampToDate, useChartWrapperSize, useSingleValueTooltip, useTouchMoveEffect @@ -23,6 +23,7 @@ import { import "./Chart.scss"; interface LineChartProps { + chartId: string; title?: string; info?: ModalData; data: { [name: string]: number; time: number }[]; @@ -30,7 +31,7 @@ interface LineChartProps { color: string; } -const LineChart: React.FC = ({ title, info, data, label, color }) => { +const LineChart: React.FC = ({ chartId, title, info, data, label, color }) => { const [{ wrapperWidth, wrapperHeight }, setTheRef] = useChartWrapperSize(); const chartWrapperRef = useCallback((chartWrapper: HTMLDivElement) => { if (chartWrapper !== null) { @@ -53,102 +54,160 @@ const LineChart: React.FC = ({ title, info, data, label, color } data = timespan !== "all" ? data.slice(-timespan) : data; - const dataMaxY = max(data, d => d.n) ?? 1; - const leftMargin = determineGraphLeftPadding(dataMaxY); - + // chart dimensions + const yMax = max(data, d => d.n) ?? 1; + const leftMargin = determineGraphLeftPadding(yMax); const MARGIN = { top: 30, right: 20, bottom: 50, left: leftMargin }; const INNER_WIDTH = width - MARGIN.left - MARGIN.right; const INNER_HEIGHT = height - MARGIN.top - MARGIN.bottom; - const timestampToDate = (timestampInSec: number) => ( - moment.unix(timestampInSec) - .hours(0).minutes(0) - .toDate() - ); - - const domain = [ - data.length > 0 ? timestampToDate(data[0].time) : new Date(), - data.length > 0 ? timestampToDate(data[data.length - 1].time) : new Date() - ]; - - const x = scaleTime().domain(domain).range([0, INNER_WIDTH]) - .nice(); - - const y = scaleLinear().domain([0, dataMaxY]).range([INNER_HEIGHT, 0]); + const dates = data.map(d => timestampToDate(d.time)); + // SVG const svg = select(theSvg.current) .attr("viewBox", `0 0 ${width} ${height}`) .attr("preserveAspectRatio", "none") .append("g") .attr("transform", `translate(${MARGIN.left}, ${MARGIN.top})`); - const yAxisGrid = axisLeft(y.nice()).tickFormat(format(d3FormatSpecifier(dataMaxY))); + // X + const x = scaleTime() + .domain([dates[0], dates[dates.length - 1]]) + .range([0, INNER_WIDTH]); + + const buildXAxis: (scale: ScaleTime) => Axis = scale => + axisBottom(scale).tickFormat(tickMultiFormat) as Axis; + const xAxisSelection = svg.append("g") + .attr("class", "axis axis--x") + .attr("transform", `translate(0, ${INNER_HEIGHT})`) + .call(buildXAxis(x)); + + // Y + const y = scaleLinear().domain([0, yMax]).range([INNER_HEIGHT, 0]); + const yAxisGrid = axisLeft(y.nice()).tickFormat(format(d3FormatSpecifier(yMax))); svg.append("g") .attr("class", "axis axis--y") .call(yAxisGrid); - svg.append("path") + // clip path + svg.append("defs") + .append("clipPath") + .attr("id", `clip-${chartId}`) + .append("rect") + .attr("width", width) + .attr("height", height) + .attr("x", 0) + .attr("y", 0); + + // brushing + const brush = brushX() + .extent([[0, 0], [INNER_WIDTH, height]]) + .on("end", e => { + onBrushHandler(e as D3BrushEvent<{ [key: string]: number }>); + }); + + const brushSelection = svg.append("g") + .attr("class", "brush") + .call(brush); + + // line + const lineGen = line<{ [name: string]: number; time: number }>() + .x(d => x(timestampToDate(d.time)) ?? 0) + .y(d => y(d.n)); + + const lineSelection = svg.append("g") + .attr("class", "the-line") + .attr("clip-path", `url(#clip-${chartId})`) + .append("path") .datum(data) .attr("fill", "none") .attr("stroke", color) .attr("stroke-width", 1.5) - .attr( - "d", - line<{ [name: string]: number; time: number }>() - .x(d => x(timestampToDate(d.time)) ?? 0) - .y(d => y(d.n)) - ); - - svg.selectAll("circle") - .data(data) - .enter() - .append("circle") - .attr("r", 1) - .attr("fill", color) - .style("stroke", color) - .style("stroke-width", 5) - .style("stroke-opacity", 0) - .attr("transform", d => `translate(${x(timestampToDate(d.time))}, ${y(d.n)})`) - .attr("class", (_, i) => `circle-${i}`); - - const xAxis = axisLabelRotate( - axisBottom(x).tickFormat(timeFormat("%d %b")) - ); + .attr("d", lineGen); - svg.append("g") - .attr("class", "axis axis--x") - .attr("transform", `translate(0, ${INNER_HEIGHT})`) - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - .call(xAxis); + const attachPathAndCircles = () => { + svg.selectAll(".hover-circles").remove(); + svg.selectAll(".hover-lines").remove(); + const halfLineWidth = data.length > 1 ? + ((x(timestampToDate(data[1].time)) ?? 0) - (x(timestampToDate(data[0].time)) ?? 0)) / 2 : + 18; - const halfLineWidth = data.length > 1 ? - ((x(timestampToDate(data[1].time)) ?? 0) - (x(timestampToDate(data[0].time)) ?? 0)) / 2 : - 18; + svg.append("g") + .attr("class", "hover-circles") + .selectAll("g") + .data(data) + .enter() + .append("circle") + .attr("r", 0) + .attr("fill", color) + .style("stroke", color) + .style("stroke-width", 5) + .style("stroke-opacity", 0) + .attr("transform", d => `translate(${x(timestampToDate(d.time))}, ${y(d.n)})`) + .attr("class", (_, i) => `circle-${i}`); - svg.append("g") - .attr("class", "hover-lines") - .selectAll("g") - .data(data) - .enter() - .append("rect") - .attr("fill", "transparent") - .attr("x", (_, idx) => ( - idx === 0 ? 0 : (x(timestampToDate(data[idx].time)) ?? 0) - halfLineWidth - )) - .attr("y", 0) - .attr("class", (_, i) => `rect-${i}`) - .attr("height", INNER_HEIGHT) - .attr("width", (_, idx) => { - if (idx === 0) { - return halfLineWidth; - } else if (idx === data.length - 1) { - return halfLineWidth; - } - return halfLineWidth * 2; - }) - .on("mouseover", mouseoverHandler) - .on("mouseout", mouseoutHandler); + svg.append("g") + .attr("class", "hover-lines") + .selectAll("g") + .data(data) + .enter() + .append("rect") + .attr("fill", "transparent") + .attr("x", (_, idx) => ( + idx === 0 ? 0 : (x(timestampToDate(data[idx].time)) ?? 0) - halfLineWidth + )) + .attr("y", 0) + .attr("class", (_, i) => `rect-${i}`) + .attr("height", INNER_HEIGHT) + .attr("width", (_, idx) => { + if (idx === 0) { + return halfLineWidth; + } else if (idx === data.length - 1) { + return halfLineWidth; + } + return halfLineWidth * 2; + }) + .on("mouseover", mouseoverHandler) + .on("mouseout", mouseoutHandler); + }; + + attachPathAndCircles(); + + const onBrushHandler = (event: D3BrushEvent<{ [key: string]: number }>) => { + if (!event.selection) { + return; + } + const extent = event.selection; + if (!extent) { + x.domain([dates[0], dates[dates.length - 1]]); + } else { + x.domain([x.invert(extent[0] as NumberValue), x.invert(extent[1] as NumberValue)]); + // eslint-disable-next-line @typescript-eslint/unbound-method + brushSelection.call(brush.move, null); + } + + // Update axis, area and lines position + xAxisSelection.transition().duration(1000).call(buildXAxis(x)); + lineSelection + .transition() + .duration(750) + .attr("d", lineGen); + + // rebuild the hover activated lines & cicles + attachPathAndCircles(); + }; + + // double click reset + svg.on("dblclick", () => { + x.domain([dates[0], dates[dates.length - 1]]); + xAxisSelection.transition().call(axisBottom(x).tickFormat(tickMultiFormat)); + lineSelection + .transition() + .duration(500) + .attr("d", lineGen); + attachPathAndCircles(); + }); } }, [data, timespan, wrapperWidth, wrapperHeight]); @@ -199,7 +258,7 @@ const LineChart: React.FC = ({ title, info, data, label, color } select(theSvg.current) .selectAll(`.circle-${idx}`) - .attr("r", 1) + .attr("r", 0) .style("stroke-opacity", 0); activeElement diff --git a/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx b/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx index ad9600e54..9d0dd7ff4 100644 --- a/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx @@ -5,7 +5,6 @@ import { format } from "d3-format"; import { NumberValue, scaleLinear, scaleOrdinal, scaleTime } from "d3-scale"; import { BaseType, select } from "d3-selection"; import { SeriesPoint, stack } from "d3-shape"; -import moment from "moment"; import React, { useCallback, useLayoutEffect, useRef, useState } from "react"; import { ModalData } from "../../../ModalProps"; import ChartHeader, { TimespanOption } from "../ChartHeader"; @@ -17,7 +16,8 @@ import { determineGraphLeftPadding, d3FormatSpecifier, useTouchMoveEffect, - tickMultiFormat + tickMultiFormat, + timestampToDate } from "../ChartUtils"; import "./Chart.scss"; @@ -79,11 +79,6 @@ const StackedBarChart: React.FC = ({ const color = scaleOrdinal().domain(subgroups).range(colors); - const timestampToDate = (timestamp: number) => moment.unix(timestamp) - .hours(0) - .minutes(0) - .toDate(); - const groups = data.map( d => timestampToDate(d.time) ); @@ -167,7 +162,6 @@ const StackedBarChart: React.FC = ({ if (!extent) { x.domain([groups[0], groups[groups.length - 1]]); } else { - console.log(extent); x.domain([x.invert(extent[0] as NumberValue), x.invert(extent[1] as NumberValue)]); // eslint-disable-next-line @typescript-eslint/unbound-method brushSelection.call(brush.move, null); diff --git a/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx b/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx index 02bb6bede..44d7f4a62 100644 --- a/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx @@ -5,7 +5,6 @@ import { format } from "d3-format"; import { scaleTime, scaleLinear, scaleOrdinal, NumberValue, ScaleTime } from "d3-scale"; import { BaseType, select } from "d3-selection"; import { area, line, SeriesPoint, stack } from "d3-shape"; -import moment from "moment"; import React, { useCallback, useLayoutEffect, useRef, useState } from "react"; import { ModalData } from "../../../ModalProps"; import ChartHeader, { TimespanOption } from "../ChartHeader"; @@ -15,6 +14,7 @@ import { determineGraphLeftPadding, noDataView, tickMultiFormat, + timestampToDate, useChartWrapperSize, useMultiValueTooltip, useTouchMoveEffect @@ -69,18 +69,10 @@ const StackedLineChart: React.FC = ({ const INNER_WIDTH = width - MARGIN.left - MARGIN.right; const INNER_HEIGHT = height - MARGIN.top - MARGIN.bottom; - const timestampToDate = (timestamp: number) => moment.unix(timestamp) - .hours(0) - .minutes(0) - .toDate(); - const color = scaleOrdinal().domain(subgroups).range(colors); const stackedData = stack().keys(subgroups)(data); - - const groups = data.map( - d => timestampToDate(d.time) - ); + const groups = data.map(d => timestampToDate(d.time)); // SVG const svg = select(theSvg.current) From e1cbcf283264374974029ce6bdbcaf5d851b1146 Mon Sep 17 00:00:00 2001 From: Mario Sarcevic Date: Thu, 27 Apr 2023 10:12:53 +0200 Subject: [PATCH 06/14] feat(client): Scale Y axis in graphs when "zooming" for all statistics page charts --- .../stardust/statistics/ChartUtils.tsx | 33 ++++++++++- .../stardust/statistics/charts/BarChart.tsx | 44 +++++---------- .../stardust/statistics/charts/LineChart.tsx | 31 ++++++----- .../statistics/charts/StackedBarChart.tsx | 55 +++++++++---------- .../statistics/charts/StackedLineChart.tsx | 31 +++++------ 5 files changed, 105 insertions(+), 89 deletions(-) diff --git a/client/src/app/components/stardust/statistics/ChartUtils.tsx b/client/src/app/components/stardust/statistics/ChartUtils.tsx index dd05d1b66..4c8e451ec 100644 --- a/client/src/app/components/stardust/statistics/ChartUtils.tsx +++ b/client/src/app/components/stardust/statistics/ChartUtils.tsx @@ -1,5 +1,7 @@ import { INodeInfoBaseToken } from "@iota/iota.js-stardust"; -import { NumberValue, ScaleBand } from "d3-scale"; +import { Axis, axisBottom, axisLeft } from "d3-axis"; +import { format } from "d3-format"; +import { NumberValue, ScaleBand, ScaleLinear, ScaleTime } from "d3-scale"; import { timeDay, timeMonth, timeWeek, timeYear } from "d3-time"; import { timeFormat } from "d3-time-format"; import moment from "moment"; @@ -186,8 +188,37 @@ export const tickMultiFormat = (date: Date | NumberValue) => { return formatYear(theDate); }; +export const buildXAxis: (scale: ScaleTime) => Axis = scale => + axisBottom(scale).tickFormat(tickMultiFormat) as Axis; + +export const buildYAxis = (scale: ScaleLinear, theYMax: number) => + axisLeft(scale.nice()).tickFormat(format(d3FormatSpecifier(theYMax))); + export const timestampToDate = (timestamp: number) => moment.unix(timestamp) .hours(0) .minutes(0) .toDate(); +export const computeDataIncludedInSelection = ( + scale: ScaleTime, + data: { + [name: string]: number; + time: number; + }[] +) => { + const selectedData: { [name: string]: number; time: number }[] = []; + + const from = scale.domain()[0]; + from.setHours(0, 0, 0, 0); + const to = scale.domain()[1]; + to.setHours(0, 0, 0, 0); + for (const d of data) { + const target = timestampToDate(d.time); + target.setHours(0, 0, 0, 0); + if (from <= target && target <= to) { + selectedData.push(d); + } + } + + return selectedData; +}; diff --git a/client/src/app/components/stardust/statistics/charts/BarChart.tsx b/client/src/app/components/stardust/statistics/charts/BarChart.tsx index 4a6c97f69..51f03f4bd 100644 --- a/client/src/app/components/stardust/statistics/charts/BarChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/BarChart.tsx @@ -1,19 +1,18 @@ import classNames from "classnames"; import { max } from "d3-array"; -import { Axis, axisBottom, axisLeft } from "d3-axis"; import { brushX, D3BrushEvent } from "d3-brush"; -import { format } from "d3-format"; -import { NumberValue, scaleLinear, ScaleTime, scaleTime } from "d3-scale"; +import { NumberValue, scaleLinear, scaleTime } from "d3-scale"; import { BaseType, select } from "d3-selection"; import React, { useCallback, useLayoutEffect, useRef, useState } from "react"; import { ModalData } from "../../../ModalProps"; import ChartHeader, { TimespanOption } from "../ChartHeader"; import ChartTooltip from "../ChartTooltip"; import { - d3FormatSpecifier, + buildXAxis, + buildYAxis, + computeDataIncludedInSelection, determineGraphLeftPadding, noDataView, - tickMultiFormat, timestampToDate, useChartWrapperSize, useSingleValueTooltip, @@ -73,10 +72,6 @@ const BarChart: React.FC = ({ chartId, title, info, data, label, const x = scaleTime() .domain([dates[0], dates[dates.length - 1]]) .range([0, INNER_WIDTH]); - - const buildXAxis: (scale: ScaleTime) => Axis = scale => - axisBottom(scale).tickFormat(tickMultiFormat) as Axis; - const xAxisSelection = svg.append("g") .attr("class", "axis axis--x") .attr("transform", `translate(0, ${INNER_HEIGHT})`) @@ -85,11 +80,9 @@ const BarChart: React.FC = ({ chartId, title, info, data, label, // Y const y = scaleLinear().domain([0, yMax]) .range([INNER_HEIGHT, 0]); - const yAxisGrid = axisLeft(y.nice()).tickFormat(format(d3FormatSpecifier(yMax))); - - svg.append("g") + const yAxisSelection = svg.append("g") .attr("class", "axis axis--y") - .call(yAxisGrid); + .call(buildYAxis(y, yMax)); // clip path svg.append("defs") @@ -145,30 +138,23 @@ const BarChart: React.FC = ({ chartId, title, info, data, label, brushSelection.call(brush.move, null); } - // compute bars count included in barsSelection - const from = x.domain()[0]; - from.setHours(0, 0, 0, 0); - const to = x.domain()[1]; - to.setHours(0, 0, 0, 0); - let barsCount = 0; - for (const d of data) { - const target = timestampToDate(d.time); - target.setHours(0, 0, 0, 0); - if (from <= target && target <= to) { - barsCount++; - } - } + const selectedData = computeDataIncludedInSelection(x, data); + const yMaxUpdate = max(selectedData, d => d.n) ?? 1; + y.domain([0, yMaxUpdate]); + yAxisSelection.transition().duration(750).call(buildYAxis(y, yMaxUpdate)); // Update bars - renderBars(barsCount); + renderBars(selectedData.length); // Update axis, area and lines position - xAxisSelection.transition().duration(1000).call(axisBottom(x).tickFormat(tickMultiFormat)); + xAxisSelection.transition().duration(750).call(buildXAxis(x)); }; // double click reset svg.on("dblclick", () => { x.domain([dates[0], dates[dates.length - 1]]); - xAxisSelection.transition().call(axisBottom(x).tickFormat(tickMultiFormat)); + xAxisSelection.transition().duration(750).call(buildXAxis(x)); + y.domain([0, yMax]); + yAxisSelection.transition().duration(750).call(buildYAxis(y, yMax)); renderBars(data.length); }); } diff --git a/client/src/app/components/stardust/statistics/charts/LineChart.tsx b/client/src/app/components/stardust/statistics/charts/LineChart.tsx index 7899b4b29..9b17e9438 100644 --- a/client/src/app/components/stardust/statistics/charts/LineChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/LineChart.tsx @@ -1,9 +1,7 @@ import classNames from "classnames"; import { max } from "d3-array"; -import { Axis, axisBottom, axisLeft } from "d3-axis"; import { brushX, D3BrushEvent } from "d3-brush"; -import { format } from "d3-format"; -import { NumberValue, scaleLinear, ScaleTime, scaleTime } from "d3-scale"; +import { NumberValue, scaleLinear, scaleTime } from "d3-scale"; import { BaseType, select } from "d3-selection"; import { line } from "d3-shape"; import React, { useCallback, useLayoutEffect, useRef, useState } from "react"; @@ -11,10 +9,11 @@ import { ModalData } from "../../../ModalProps"; import ChartHeader, { TimespanOption } from "../ChartHeader"; import ChartTooltip from "../ChartTooltip"; import { - d3FormatSpecifier, + buildXAxis, + buildYAxis, + computeDataIncludedInSelection, determineGraphLeftPadding, noDataView, - tickMultiFormat, timestampToDate, useChartWrapperSize, useSingleValueTooltip, @@ -74,10 +73,6 @@ const LineChart: React.FC = ({ chartId, title, info, data, label const x = scaleTime() .domain([dates[0], dates[dates.length - 1]]) .range([0, INNER_WIDTH]); - - const buildXAxis: (scale: ScaleTime) => Axis = scale => - axisBottom(scale).tickFormat(tickMultiFormat) as Axis; - const xAxisSelection = svg.append("g") .attr("class", "axis axis--x") .attr("transform", `translate(0, ${INNER_HEIGHT})`) @@ -85,10 +80,9 @@ const LineChart: React.FC = ({ chartId, title, info, data, label // Y const y = scaleLinear().domain([0, yMax]).range([INNER_HEIGHT, 0]); - const yAxisGrid = axisLeft(y.nice()).tickFormat(format(d3FormatSpecifier(yMax))); - svg.append("g") + const yAxisSelection = svg.append("g") .attr("class", "axis axis--y") - .call(yAxisGrid); + .call(buildYAxis(y, yMax)); // clip path svg.append("defs") @@ -187,6 +181,15 @@ const LineChart: React.FC = ({ chartId, title, info, data, label brushSelection.call(brush.move, null); } + const from = x.domain()[0]; + from.setHours(0, 0, 0, 0); + const to = x.domain()[1]; + to.setHours(0, 0, 0, 0); + const selectedData = computeDataIncludedInSelection(x, data); + const yMaxUpdate = max(selectedData, d => d.n) ?? 1; + y.domain([0, yMaxUpdate]); + yAxisSelection.transition().duration(750).call(buildYAxis(y, yMaxUpdate)); + // Update axis, area and lines position xAxisSelection.transition().duration(1000).call(buildXAxis(x)); lineSelection @@ -201,7 +204,9 @@ const LineChart: React.FC = ({ chartId, title, info, data, label // double click reset svg.on("dblclick", () => { x.domain([dates[0], dates[dates.length - 1]]); - xAxisSelection.transition().call(axisBottom(x).tickFormat(tickMultiFormat)); + xAxisSelection.transition().call(buildXAxis(x)); + y.domain([0, yMax]); + yAxisSelection.transition().duration(750).call(buildYAxis(y, yMax)); lineSelection .transition() .duration(500) diff --git a/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx b/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx index 9d0dd7ff4..ca85cb214 100644 --- a/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx @@ -1,7 +1,5 @@ import classNames from "classnames"; -import { axisBottom, axisLeft } from "d3-axis"; import { brushX, D3BrushEvent } from "d3-brush"; -import { format } from "d3-format"; import { NumberValue, scaleLinear, scaleOrdinal, scaleTime } from "d3-scale"; import { BaseType, select } from "d3-selection"; import { SeriesPoint, stack } from "d3-shape"; @@ -14,10 +12,11 @@ import { noDataView, useChartWrapperSize, determineGraphLeftPadding, - d3FormatSpecifier, useTouchMoveEffect, - tickMultiFormat, - timestampToDate + timestampToDate, + buildXAxis, + buildYAxis, + computeDataIncludedInSelection } from "../ChartUtils"; import "./Chart.scss"; @@ -95,21 +94,16 @@ const StackedBarChart: React.FC = ({ const x = scaleTime() .domain([groups[0], groups[groups.length - 1]]) .range([0, INNER_WIDTH]); - - const xAxis = axisBottom(x).tickFormat(tickMultiFormat); - const xAxisSelection = svg.append("g") .attr("class", "axis axis--x") .attr("transform", `translate(0, ${INNER_HEIGHT})`) - .call(xAxis); + .call(buildXAxis(x)); // Y - const y = scaleLinear().domain([0, yMax]) - .range([INNER_HEIGHT, 0]); - const yAxisGrid = axisLeft(y.nice()).tickFormat(format(d3FormatSpecifier(yMax))); - svg.append("g") + const y = scaleLinear().domain([0, yMax]).range([INNER_HEIGHT, 0]); + const yAxisSelection = svg.append("g") .attr("class", "axis axis--y") - .call(yAxisGrid); + .call(buildYAxis(y, yMax)); // clip path svg.append("defs") @@ -167,30 +161,31 @@ const StackedBarChart: React.FC = ({ brushSelection.call(brush.move, null); } - // compute bars count included in barsSelection - const from = x.domain()[0]; - from.setHours(0, 0, 0, 0); - const to = x.domain()[1]; - to.setHours(0, 0, 0, 0); - let barsCount = 0; - for (const d of data) { - const target = timestampToDate(d.time); - target.setHours(0, 0, 0, 0); - if (from <= target && target <= to) { - barsCount++; - } - } + const selectedData = computeDataIncludedInSelection(x, data); + const yMaxUpdate = Math.max( + ...selectedData.map(d => { + let sum = 0; + for (const key of subgroups) { + sum += d[key]; + } + return sum; + }) + ); + y.domain([0, yMaxUpdate]); + yAxisSelection.transition().duration(750).call(buildYAxis(y, yMaxUpdate)); // Update bars - renderBars(barsCount); + renderBars(selectedData.length); // Update axis, area and lines position - xAxisSelection.transition().duration(1000).call(axisBottom(x).tickFormat(tickMultiFormat)); + xAxisSelection.transition().duration(1000).call(buildXAxis(x)); }; // double click reset svg.on("dblclick", () => { x.domain([groups[0], groups[groups.length - 1]]); - xAxisSelection.transition().call(axisBottom(x).tickFormat(tickMultiFormat)); + xAxisSelection.transition().call(buildXAxis(x)); + y.domain([0, yMax]); + yAxisSelection.transition().duration(750).call(buildYAxis(y, yMax)); renderBars(data.length); }); } diff --git a/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx b/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx index 44d7f4a62..d139030fc 100644 --- a/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx @@ -1,8 +1,6 @@ import classNames from "classnames"; -import { Axis, axisBottom, axisLeft } from "d3-axis"; import { brushX, D3BrushEvent } from "d3-brush"; -import { format } from "d3-format"; -import { scaleTime, scaleLinear, scaleOrdinal, NumberValue, ScaleTime } from "d3-scale"; +import { scaleTime, scaleLinear, scaleOrdinal, NumberValue } from "d3-scale"; import { BaseType, select } from "d3-selection"; import { area, line, SeriesPoint, stack } from "d3-shape"; import React, { useCallback, useLayoutEffect, useRef, useState } from "react"; @@ -10,10 +8,11 @@ import { ModalData } from "../../../ModalProps"; import ChartHeader, { TimespanOption } from "../ChartHeader"; import ChartTooltip from "../ChartTooltip"; import { - d3FormatSpecifier, + buildXAxis, + buildYAxis, + computeDataIncludedInSelection, determineGraphLeftPadding, noDataView, - tickMultiFormat, timestampToDate, useChartWrapperSize, useMultiValueTooltip, @@ -86,22 +85,16 @@ const StackedLineChart: React.FC = ({ .domain([groups[0], groups[groups.length - 1]]) .range([0, INNER_WIDTH]); - const buildXAxis: (scale: ScaleTime) => Axis = scale => - axisBottom(scale).tickFormat(tickMultiFormat) as Axis; - const xAxisSelection = svg.append("g") .attr("class", "axis axis--x") .attr("transform", `translate(0, ${INNER_HEIGHT})`) .call(buildXAxis(x)); // Y - const y = scaleLinear().domain([0, yMax]) - .range([INNER_HEIGHT, 0]); - - const yAxisGrid = axisLeft(y.nice()).tickFormat(format(d3FormatSpecifier(yMax))); - svg.append("g") + const y = scaleLinear().domain([0, yMax]).range([INNER_HEIGHT, 0]); + const yAxisSelection = svg.append("g") .attr("class", "axis axis--y") - .call(yAxisGrid); + .call(buildYAxis(y, yMax)); // clip path svg.append("defs") @@ -226,8 +219,13 @@ const StackedLineChart: React.FC = ({ brushSelection.call(brush.move, null); } - // Update axis, area and lines position + const selectedData = computeDataIncludedInSelection(x, data); + const yMaxUpdate = Math.max(...selectedData.map(d => Math.max(...subgroups.map(key => d[key])))); + y.domain([0, yMaxUpdate]); + // Update axis + yAxisSelection.transition().duration(750).call(buildYAxis(y, yMaxUpdate)); xAxisSelection.transition().duration(1000).call(buildXAxis(x)); + // Update area and lines areaSelection .transition() .duration(750) @@ -236,7 +234,6 @@ const StackedLineChart: React.FC = ({ .transition() .duration(750) .attr("d", lineGen); - // rebuild the hover activated lines & cicles attachOnHoverLinesAndCircles(); }; @@ -245,6 +242,8 @@ const StackedLineChart: React.FC = ({ svg.on("dblclick", () => { x.domain([groups[0], groups[groups.length - 1]]); xAxisSelection.transition().call(buildXAxis(x)); + y.domain([0, yMax]); + yAxisSelection.transition().duration(750).call(buildYAxis(y, yMax)); areaSelection .transition() .duration(500) From e63fd497ddde6e9e5a03be4bb9ce353c1fbae225 Mon Sep 17 00:00:00 2001 From: Mario Sarcevic Date: Thu, 27 Apr 2023 10:16:34 +0200 Subject: [PATCH 07/14] feat(client): Remove unused functions (ts-prune) --- .../stardust/statistics/ChartUtils.tsx | 21 ++----------------- .../api/stardust/feed/IFeedBlockData.ts | 2 +- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/client/src/app/components/stardust/statistics/ChartUtils.tsx b/client/src/app/components/stardust/statistics/ChartUtils.tsx index 4c8e451ec..06f1b2553 100644 --- a/client/src/app/components/stardust/statistics/ChartUtils.tsx +++ b/client/src/app/components/stardust/statistics/ChartUtils.tsx @@ -1,16 +1,13 @@ import { INodeInfoBaseToken } from "@iota/iota.js-stardust"; import { Axis, axisBottom, axisLeft } from "d3-axis"; import { format } from "d3-format"; -import { NumberValue, ScaleBand, ScaleLinear, ScaleTime } from "d3-scale"; +import { NumberValue, ScaleLinear, ScaleTime } from "d3-scale"; import { timeDay, timeMonth, timeWeek, timeYear } from "d3-time"; import { timeFormat } from "d3-time-format"; import moment from "moment"; import React, { useCallback, useEffect, useState } from "react"; import { formatAmount } from "../../../../helpers/stardust/valueFormatHelper"; import { IDistributionEntry } from "../../../../models/api/stardust/chronicle/ITokenDistributionResponse"; -import { TimespanOption } from "./ChartHeader"; - -export const DAY_LABEL_FORMAT = "DD MMM"; export const noDataView = () => (
@@ -152,27 +149,13 @@ export const getSubunitThreshold = (tokenInfo: INodeInfoBaseToken) => ( Math.pow(10, tokenInfo.decimals) : null ); -export const barChartsTickValues = ( - timespan: TimespanOption, - axisBand: ScaleBand -) => ( - timespan === "all" ? - axisBand.domain().filter(d => d.includes("01")) : - ( - timespan === "7" ? - axisBand.domain() : - // every third label - axisBand.domain().filter((_, i) => !(i % 3)) - ) -); - const formatHidden = timeFormat(""); const formatDay = timeFormat("%a %d"); const formatWeek = timeFormat("%b %d"); const formatMonth = timeFormat("%B"); const formatYear = timeFormat("%Y"); -export const tickMultiFormat = (date: Date | NumberValue) => { +const tickMultiFormat = (date: Date | NumberValue) => { const theDate = date as Date; if (timeDay(theDate) < theDate) { return formatHidden(theDate); diff --git a/client/src/models/api/stardust/feed/IFeedBlockData.ts b/client/src/models/api/stardust/feed/IFeedBlockData.ts index fd557c088..031e792e4 100644 --- a/client/src/models/api/stardust/feed/IFeedBlockData.ts +++ b/client/src/models/api/stardust/feed/IFeedBlockData.ts @@ -1,7 +1,7 @@ import { HexEncodedString, IMilestonePayload } from "@iota/iota.js-stardust"; import { IFeedBlockMetadata } from "./IFeedBlockMetadata"; -export interface IFeedBlockProperties { +interface IFeedBlockProperties { index?: number; tag?: HexEncodedString; timestamp?: number; From 4de038f64f0ff9647e6cac346396a0d57987f6de Mon Sep 17 00:00:00 2001 From: Mario Sarcevic Date: Thu, 27 Apr 2023 10:25:15 +0200 Subject: [PATCH 08/14] feat(client): Remove Timespan selector component --- .../stardust/statistics/ChartHeader.scss | 31 ------------------- .../stardust/statistics/ChartHeader.tsx | 29 +---------------- .../stardust/statistics/charts/BarChart.tsx | 10 ++---- .../stardust/statistics/charts/LineChart.tsx | 10 ++---- .../statistics/charts/StackedBarChart.tsx | 10 ++---- .../statistics/charts/StackedLineChart.tsx | 10 ++---- 6 files changed, 13 insertions(+), 87 deletions(-) diff --git a/client/src/app/components/stardust/statistics/ChartHeader.scss b/client/src/app/components/stardust/statistics/ChartHeader.scss index f69d643d8..19b25c8ec 100644 --- a/client/src/app/components/stardust/statistics/ChartHeader.scss +++ b/client/src/app/components/stardust/statistics/ChartHeader.scss @@ -32,37 +32,6 @@ } } - .chart-header__select { - display: flex; - justify-content: center; - align-items: center; - .select-wrapper { - position: relative; - margin-right: 20px; - - select { - cursor: pointer; - background: var(--statistics-select-bg); - color: var(--card-color); - border-color: var(--statistics-select-border); - height: 40px; - - @include phone-down { - height: 32px; - } - } - - .chevron { - top: 23%; - color: var(--card-color); - z-index: 1; - - @include phone-down { - top: 13%; - } - } - } - } .charts-css.legend { padding: 1rem; border: 1px solid var(--legend-border-color); diff --git a/client/src/app/components/stardust/statistics/ChartHeader.tsx b/client/src/app/components/stardust/statistics/ChartHeader.tsx index 93fa61859..f5f0bee1e 100644 --- a/client/src/app/components/stardust/statistics/ChartHeader.tsx +++ b/client/src/app/components/stardust/statistics/ChartHeader.tsx @@ -1,4 +1,3 @@ -import classNames from "classnames"; import React from "react"; import Modal from "../../Modal"; import { ModalData } from "../../ModalProps"; @@ -8,7 +7,6 @@ import "./ChartHeader.scss"; interface ChartHeaderProps { title?: string; info?: ModalData; - onTimespanSelected: (value: TimespanOption) => void; disabled?: boolean; legend?: { labels: string[]; @@ -16,9 +14,7 @@ interface ChartHeaderProps { }; } -export type TimespanOption = "7" | "30" | "90" | "all"; - -const ChartHeader: React.FC = ({ title, info, onTimespanSelected, disabled, legend }) => ( +const ChartHeader: React.FC = ({ title, info, disabled, legend }) => (
{title && ( @@ -29,29 +25,6 @@ const ChartHeader: React.FC = ({ title, info, onTimespanSelect )}
)} - - {!disabled && ( -
-
- - - expand_more - -
-
- )}
{!disabled && legend && ( diff --git a/client/src/app/components/stardust/statistics/charts/BarChart.tsx b/client/src/app/components/stardust/statistics/charts/BarChart.tsx index 51f03f4bd..d0fba7a07 100644 --- a/client/src/app/components/stardust/statistics/charts/BarChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/BarChart.tsx @@ -3,9 +3,9 @@ import { max } from "d3-array"; import { brushX, D3BrushEvent } from "d3-brush"; import { NumberValue, scaleLinear, scaleTime } from "d3-scale"; import { BaseType, select } from "d3-selection"; -import React, { useCallback, useLayoutEffect, useRef, useState } from "react"; +import React, { useCallback, useLayoutEffect, useRef } from "react"; import { ModalData } from "../../../ModalProps"; -import ChartHeader, { TimespanOption } from "../ChartHeader"; +import ChartHeader from "../ChartHeader"; import ChartTooltip from "../ChartTooltip"; import { buildXAxis, @@ -38,7 +38,6 @@ const BarChart: React.FC = ({ chartId, title, info, data, label, }, []); const theTooltip = useRef(null); const theSvg = useRef(null); - const [timespan, setTimespan] = useState("all"); const buildTooltip = useSingleValueTooltip(data, label); useTouchMoveEffect(mouseoutHandler); @@ -50,8 +49,6 @@ const BarChart: React.FC = ({ chartId, title, info, data, label, // reset select(theSvg.current).select("*").remove(); - data = timespan !== "all" ? data.slice(-timespan) : data; - // chart dimensions const yMax = max(data, d => d.n) ?? 1; const leftMargin = determineGraphLeftPadding(yMax); @@ -158,7 +155,7 @@ const BarChart: React.FC = ({ chartId, title, info, data, label, renderBars(data.length); }); } - }, [data, timespan, wrapperWidth, wrapperHeight]); + }, [data, wrapperWidth, wrapperHeight]); /** * Handles mouseover event of a bar "part" @@ -197,7 +194,6 @@ const BarChart: React.FC = ({ chartId, title, info, data, label, setTimespan(value)} disabled={data.length === 0} /> {data.length === 0 ? ( diff --git a/client/src/app/components/stardust/statistics/charts/LineChart.tsx b/client/src/app/components/stardust/statistics/charts/LineChart.tsx index 9b17e9438..88fe72d06 100644 --- a/client/src/app/components/stardust/statistics/charts/LineChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/LineChart.tsx @@ -4,9 +4,9 @@ import { brushX, D3BrushEvent } from "d3-brush"; import { NumberValue, scaleLinear, scaleTime } from "d3-scale"; import { BaseType, select } from "d3-selection"; import { line } from "d3-shape"; -import React, { useCallback, useLayoutEffect, useRef, useState } from "react"; +import React, { useCallback, useLayoutEffect, useRef } from "react"; import { ModalData } from "../../../ModalProps"; -import ChartHeader, { TimespanOption } from "../ChartHeader"; +import ChartHeader from "../ChartHeader"; import ChartTooltip from "../ChartTooltip"; import { buildXAxis, @@ -39,7 +39,6 @@ const LineChart: React.FC = ({ chartId, title, info, data, label }, []); const theSvg = useRef(null); const theTooltip = useRef(null); - const [timespan, setTimespan] = useState("all"); const buildTooltip = useSingleValueTooltip(data, label); useTouchMoveEffect(mouseoutHandler); @@ -51,8 +50,6 @@ const LineChart: React.FC = ({ chartId, title, info, data, label // reset select(theSvg.current).select("*").remove(); - data = timespan !== "all" ? data.slice(-timespan) : data; - // chart dimensions const yMax = max(data, d => d.n) ?? 1; const leftMargin = determineGraphLeftPadding(yMax); @@ -214,7 +211,7 @@ const LineChart: React.FC = ({ chartId, title, info, data, label attachPathAndCircles(); }); } - }, [data, timespan, wrapperWidth, wrapperHeight]); + }, [data, wrapperWidth, wrapperHeight]); /** * Handles mouseover event. @@ -276,7 +273,6 @@ const LineChart: React.FC = ({ chartId, title, info, data, label setTimespan(value)} disabled={data.length === 0} /> {data.length === 0 ? ( diff --git a/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx b/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx index ca85cb214..ff6ed5be2 100644 --- a/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx @@ -3,9 +3,9 @@ import { brushX, D3BrushEvent } from "d3-brush"; import { NumberValue, scaleLinear, scaleOrdinal, scaleTime } from "d3-scale"; import { BaseType, select } from "d3-selection"; import { SeriesPoint, stack } from "d3-shape"; -import React, { useCallback, useLayoutEffect, useRef, useState } from "react"; +import React, { useCallback, useLayoutEffect, useRef } from "react"; import { ModalData } from "../../../ModalProps"; -import ChartHeader, { TimespanOption } from "../ChartHeader"; +import ChartHeader from "../ChartHeader"; import ChartTooltip from "../ChartTooltip"; import { useMultiValueTooltip, @@ -47,7 +47,6 @@ const StackedBarChart: React.FC = ({ }, []); const theSvg = useRef(null); const theTooltip = useRef(null); - const [timespan, setTimespan] = useState("all"); const buildTooltip = useMultiValueTooltip(data, subgroups, colors, groupLabels); useTouchMoveEffect(mouseoutHandler); @@ -59,8 +58,6 @@ const StackedBarChart: React.FC = ({ // reset select(theSvg.current).select("*").remove(); - data = timespan !== "all" ? data.slice(-timespan) : data; - // chart dimensions const yMax = Math.max( ...data.map(d => { @@ -189,7 +186,7 @@ const StackedBarChart: React.FC = ({ renderBars(data.length); }); } - }, [data, timespan, wrapperWidth, wrapperHeight]); + }, [data, wrapperWidth, wrapperHeight]); /** * Handles mouseover event of a bar "part" @@ -232,7 +229,6 @@ const StackedBarChart: React.FC = ({ labels: groupLabels ?? subgroups, colors }} - onTimespanSelected={value => setTimespan(value)} disabled={data.length === 0} /> {data.length === 0 ? ( diff --git a/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx b/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx index d139030fc..aae73d2ee 100644 --- a/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx @@ -3,9 +3,9 @@ import { brushX, D3BrushEvent } from "d3-brush"; import { scaleTime, scaleLinear, scaleOrdinal, NumberValue } from "d3-scale"; import { BaseType, select } from "d3-selection"; import { area, line, SeriesPoint, stack } from "d3-shape"; -import React, { useCallback, useLayoutEffect, useRef, useState } from "react"; +import React, { useCallback, useLayoutEffect, useRef } from "react"; import { ModalData } from "../../../ModalProps"; -import ChartHeader, { TimespanOption } from "../ChartHeader"; +import ChartHeader from "../ChartHeader"; import ChartTooltip from "../ChartTooltip"; import { buildXAxis, @@ -47,7 +47,6 @@ const StackedLineChart: React.FC = ({ }, []); const theSvg = useRef(null); const theTooltip = useRef(null); - const [timespan, setTimespan] = useState("all"); const buildTootip = useMultiValueTooltip(data, subgroups, colors, groupLabels); useTouchMoveEffect(mouseoutHandler); @@ -59,8 +58,6 @@ const StackedLineChart: React.FC = ({ // reset select(theSvg.current).selectAll("*").remove(); - data = timespan !== "all" ? data.slice(-timespan) : data; - // chart dimensions const yMax = Math.max(...data.map(d => Math.max(...subgroups.map(key => d[key])))); const leftMargin = determineGraphLeftPadding(yMax); @@ -258,7 +255,7 @@ const StackedLineChart: React.FC = ({ attachOnHoverLinesAndCircles(); } - }, [data, timespan, wrapperWidth, wrapperHeight]); + }, [data, wrapperWidth, wrapperHeight]); /** * Get linear gradient for selected color @@ -348,7 +345,6 @@ const StackedLineChart: React.FC = ({ setTimespan(value)} legend={{ labels: groupLabels ?? subgroups, colors From 1c0b46d2c17c1d98977c87c280bb23ae6cb062b5 Mon Sep 17 00:00:00 2001 From: Mario Sarcevic Date: Thu, 27 Apr 2023 10:26:44 +0200 Subject: [PATCH 09/14] feat(client): Switch Total ledger size graph to StackedLineChart --- .../src/app/components/stardust/statistics/InfluxChartsTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/app/components/stardust/statistics/InfluxChartsTab.tsx b/client/src/app/components/stardust/statistics/InfluxChartsTab.tsx index 174ccf09d..9c310ec76 100644 --- a/client/src/app/components/stardust/statistics/InfluxChartsTab.tsx +++ b/client/src/app/components/stardust/statistics/InfluxChartsTab.tsx @@ -226,7 +226,7 @@ export const InfluxChartsTab: React.FC = () => {
- Date: Thu, 27 Apr 2023 10:39:38 +0200 Subject: [PATCH 10/14] feat(client): Format values to magnitudes in tooltips --- .../src/app/components/stardust/statistics/ChartUtils.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/src/app/components/stardust/statistics/ChartUtils.tsx b/client/src/app/components/stardust/statistics/ChartUtils.tsx index 06f1b2553..93beca1ea 100644 --- a/client/src/app/components/stardust/statistics/ChartUtils.tsx +++ b/client/src/app/components/stardust/statistics/ChartUtils.tsx @@ -15,6 +15,8 @@ export const noDataView = () => (
); + +const formatToMagnituge = (val: number) => format(d3FormatSpecifier(val)); export const useSingleValueTooltip = ( data: { [name: string]: number; time: number }[], label?: string @@ -24,14 +26,13 @@ export const useSingleValueTooltip = (

${moment.unix(dataPoint.time).format("DD-MM-YYYY")}

${label ?? "count"}: - ${dataPoint.n} + ${formatToMagnituge(dataPoint.n)(dataPoint.n)}

` ), [data, label]); return buildTooltip; }; - export const useMultiValueTooltip = ( data: { [name: string]: number; time: number }[], subgroups: string[], @@ -44,7 +45,7 @@ export const useMultiValueTooltip = (

${groupLabels ? groupLabels[idx] : subgroup}: - ${dataPoint[subgroup]} + ${formatToMagnituge(dataPoint[subgroup])(dataPoint[subgroup])}

`)).join("")}` ), [data, subgroups, groupLabels, colors]); From e6ac870461e5516198dfa9de1f2c34fae6c52877 Mon Sep 17 00:00:00 2001 From: Mario Sarcevic Date: Thu, 27 Apr 2023 10:59:12 +0200 Subject: [PATCH 11/14] feat(client): Add transition when zooming bars also for StackedBarChart and BarChart --- .../stardust/statistics/charts/BarChart.tsx | 48 ++++++++++--------- .../statistics/charts/StackedBarChart.tsx | 47 +++++++++--------- 2 files changed, 51 insertions(+), 44 deletions(-) diff --git a/client/src/app/components/stardust/statistics/charts/BarChart.tsx b/client/src/app/components/stardust/statistics/charts/BarChart.tsx index d0fba7a07..1908cedfb 100644 --- a/client/src/app/components/stardust/statistics/charts/BarChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/BarChart.tsx @@ -101,26 +101,21 @@ const BarChart: React.FC = ({ chartId, title, info, data, label, .call(brush); // bars - const renderBars = (datesLen: number) => { - svg.selectAll(".the-bars").remove(); - svg.append("g") - .attr("class", "the-bars") - .attr("clip-path", `url(#clip-${chartId})`) - .selectAll("g") - .data(data) - .enter() - .append("rect") - .attr("class", "bar") - .attr("x", d => x(timestampToDate(d.time)) - ((INNER_WIDTH / datesLen) / 2)) - .attr("y", d => y(d.n)) - .attr("fill", color) - .on("mouseover", mouseoverHandler) - .on("mouseout", mouseoutHandler) - .attr("width", INNER_WIDTH / datesLen) - .attr("height", d => INNER_HEIGHT - y(d.n)); - }; - - renderBars(data.length); + const barsSelection = svg.append("g") + .attr("class", "the-bars") + .attr("clip-path", `url(#clip-${chartId})`) + .selectAll("g") + .data(data) + .enter() + .append("rect") + .attr("class", "bar") + .attr("x", d => x(timestampToDate(d.time)) - ((INNER_WIDTH / data.length) / 2)) + .attr("y", d => y(d.n)) + .attr("fill", color) + .on("mouseover", mouseoverHandler) + .on("mouseout", mouseoutHandler) + .attr("width", INNER_WIDTH / data.length) + .attr("height", d => INNER_HEIGHT - y(d.n)); const onBrushHandler = (event: D3BrushEvent<{ [key: string]: number }>) => { if (!event.selection) { @@ -141,7 +136,12 @@ const BarChart: React.FC = ({ chartId, title, info, data, label, yAxisSelection.transition().duration(750).call(buildYAxis(y, yMaxUpdate)); // Update bars - renderBars(selectedData.length); + barsSelection.transition().duration(1000) + .attr("x", d => x(timestampToDate(d.time)) - ((INNER_WIDTH / selectedData.length) / 2)) + .attr("y", d => y(d.n)) + .attr("width", INNER_WIDTH / selectedData.length) + .attr("height", d => INNER_HEIGHT - y(d.n)); + // Update axis, area and lines position xAxisSelection.transition().duration(750).call(buildXAxis(x)); }; @@ -152,7 +152,11 @@ const BarChart: React.FC = ({ chartId, title, info, data, label, xAxisSelection.transition().duration(750).call(buildXAxis(x)); y.domain([0, yMax]); yAxisSelection.transition().duration(750).call(buildYAxis(y, yMax)); - renderBars(data.length); + barsSelection.transition().duration(1000) + .attr("x", d => x(timestampToDate(d.time)) - ((INNER_WIDTH / data.length) / 2)) + .attr("y", d => y(d.n)) + .attr("width", INNER_WIDTH / data.length) + .attr("height", d => INNER_HEIGHT - y(d.n)); }); } }, [data, wrapperWidth, wrapperHeight]); diff --git a/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx b/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx index ff6ed5be2..2e4cccf62 100644 --- a/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx @@ -124,26 +124,21 @@ const StackedBarChart: React.FC = ({ // bars const barsSelection = svg.append("g") .attr("class", "stacked-bars") - .attr("clip-path", `url(#clip-${chartId})`); - - const renderBars = (datesLen: number) => { - barsSelection.selectAll("g") - .data(stackedData) - .join("g") - .attr("fill", d => color(d.key)) - .selectAll("rect") - .data(d => d) - .join("rect") - .attr("x", d => x(timestampToDate(d.data.time)) - ((INNER_WIDTH / datesLen) / 2)) - .attr("y", d => y(d[1])) - .attr("class", (_, i) => `stacked-bar rect-${i}`) - .on("mouseover", mouseoverHandler) - .on("mouseout", mouseoutHandler) - .attr("height", d => y(d[0]) - y(d[1])) - .attr("width", INNER_WIDTH / datesLen); - }; - - renderBars(data.length); + .attr("clip-path", `url(#clip-${chartId})`) + .selectAll("g") + .data(stackedData) + .join("g") + .attr("fill", d => color(d.key)) + .selectAll("rect") + .data(d => d) + .join("rect") + .attr("x", d => x(timestampToDate(d.data.time)) - ((INNER_WIDTH / data.length) / 2)) + .attr("y", d => y(d[1])) + .attr("class", (_, i) => `stacked-bar rect-${i}`) + .on("mouseover", mouseoverHandler) + .on("mouseout", mouseoutHandler) + .attr("height", d => y(d[0]) - y(d[1])) + .attr("width", INNER_WIDTH / data.length); const onBrushHandler = (event: D3BrushEvent<{ [key: string]: number }>) => { if (!event.selection) { @@ -172,7 +167,11 @@ const StackedBarChart: React.FC = ({ yAxisSelection.transition().duration(750).call(buildYAxis(y, yMaxUpdate)); // Update bars - renderBars(selectedData.length); + barsSelection.transition().duration(1000) + .attr("x", d => x(timestampToDate(d.data.time)) - ((INNER_WIDTH / selectedData.length) / 2)) + .attr("y", d => y(d[1])) + .attr("height", d => y(d[0]) - y(d[1])) + .attr("width", INNER_WIDTH / selectedData.length); // Update axis, area and lines position xAxisSelection.transition().duration(1000).call(buildXAxis(x)); }; @@ -183,7 +182,11 @@ const StackedBarChart: React.FC = ({ xAxisSelection.transition().call(buildXAxis(x)); y.domain([0, yMax]); yAxisSelection.transition().duration(750).call(buildYAxis(y, yMax)); - renderBars(data.length); + barsSelection.transition().duration(1000) + .attr("x", d => x(timestampToDate(d.data.time)) - ((INNER_WIDTH / data.length) / 2)) + .attr("y", d => y(d[1])) + .attr("height", d => y(d[0]) - y(d[1])) + .attr("width", INNER_WIDTH / data.length); }); } }, [data, wrapperWidth, wrapperHeight]); From f7e1068264a31a1482b3c6a8cd57ae3a919ab022 Mon Sep 17 00:00:00 2001 From: Mario Sarcevic Date: Thu, 27 Apr 2023 11:07:29 +0200 Subject: [PATCH 12/14] feat(client): Add a bit of border-radius to bar charts --- .../src/app/components/stardust/statistics/charts/BarChart.tsx | 1 + .../components/stardust/statistics/charts/StackedBarChart.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/client/src/app/components/stardust/statistics/charts/BarChart.tsx b/client/src/app/components/stardust/statistics/charts/BarChart.tsx index 1908cedfb..0c3b50e66 100644 --- a/client/src/app/components/stardust/statistics/charts/BarChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/BarChart.tsx @@ -112,6 +112,7 @@ const BarChart: React.FC = ({ chartId, title, info, data, label, .attr("x", d => x(timestampToDate(d.time)) - ((INNER_WIDTH / data.length) / 2)) .attr("y", d => y(d.n)) .attr("fill", color) + .attr("rx", 2) .on("mouseover", mouseoverHandler) .on("mouseout", mouseoutHandler) .attr("width", INNER_WIDTH / data.length) diff --git a/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx b/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx index 2e4cccf62..2bb121016 100644 --- a/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx @@ -134,6 +134,7 @@ const StackedBarChart: React.FC = ({ .join("rect") .attr("x", d => x(timestampToDate(d.data.time)) - ((INNER_WIDTH / data.length) / 2)) .attr("y", d => y(d[1])) + .attr("rx", 2) .attr("class", (_, i) => `stacked-bar rect-${i}`) .on("mouseover", mouseoverHandler) .on("mouseout", mouseoutHandler) From b3ab0f6e087f7ac0848b16d0a17cef7cbac2b0cc Mon Sep 17 00:00:00 2001 From: Mario Sarcevic Date: Fri, 28 Apr 2023 19:18:09 +0200 Subject: [PATCH 13/14] chore(client/statistics): Extract some values to constants --- api/src/initServices.ts | 4 ++- .../stardust/statistics/ChartUtils.tsx | 12 +++++++ .../stardust/statistics/charts/BarChart.tsx | 13 ++++---- .../stardust/statistics/charts/LineChart.tsx | 22 +++++-------- .../statistics/charts/StackedBarChart.tsx | 13 ++++---- .../statistics/charts/StackedLineChart.tsx | 32 ++++++------------- 6 files changed, 47 insertions(+), 49 deletions(-) diff --git a/api/src/initServices.ts b/api/src/initServices.ts index 181ab18f1..743079539 100644 --- a/api/src/initServices.ts +++ b/api/src/initServices.ts @@ -26,6 +26,8 @@ import { InfluxDBService } from "./services/stardust/influx/influxDbService"; import { NodeInfoService } from "./services/stardust/nodeInfoService"; import { StardustStatsService } from "./services/stardust/stats/stardustStatsService"; +const CURRENCY_UPDATE_INTERVAL_MS = 5 * 60000; + const isKnownProtocolVersion = (networkConfig: INetwork) => networkConfig.protocolVersion === LEGACY || networkConfig.protocolVersion === CHRYSALIS || @@ -96,7 +98,7 @@ export async function initServices(config: IConfiguration) { void currencyService.update(); }; - setInterval(update, 5 * 60000); + setInterval(update, CURRENCY_UPDATE_INTERVAL_MS); await update(); } diff --git a/client/src/app/components/stardust/statistics/ChartUtils.tsx b/client/src/app/components/stardust/statistics/ChartUtils.tsx index 93beca1ea..98646691c 100644 --- a/client/src/app/components/stardust/statistics/ChartUtils.tsx +++ b/client/src/app/components/stardust/statistics/ChartUtils.tsx @@ -9,6 +9,8 @@ import React, { useCallback, useEffect, useState } from "react"; import { formatAmount } from "../../../../helpers/stardust/valueFormatHelper"; import { IDistributionEntry } from "../../../../models/api/stardust/chronicle/ITokenDistributionResponse"; +export const TRANSITIONS_DURATION_MS = 750; + export const noDataView = () => (

No Data

@@ -206,3 +208,13 @@ export const computeDataIncludedInSelection = ( return selectedData; }; + +export const computeHalfLineWidth = ( + data: { [name: string]: number; time: number }[], + x: ScaleTime +) => ( + data.length > 1 ? + ((x(timestampToDate(data[1].time)) ?? 0) - (x(timestampToDate(data[0].time)) ?? 0)) / 2 : + 0 +); + diff --git a/client/src/app/components/stardust/statistics/charts/BarChart.tsx b/client/src/app/components/stardust/statistics/charts/BarChart.tsx index 0c3b50e66..38e756623 100644 --- a/client/src/app/components/stardust/statistics/charts/BarChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/BarChart.tsx @@ -14,6 +14,7 @@ import { determineGraphLeftPadding, noDataView, timestampToDate, + TRANSITIONS_DURATION_MS, useChartWrapperSize, useSingleValueTooltip, useTouchMoveEffect @@ -134,26 +135,26 @@ const BarChart: React.FC = ({ chartId, title, info, data, label, const selectedData = computeDataIncludedInSelection(x, data); const yMaxUpdate = max(selectedData, d => d.n) ?? 1; y.domain([0, yMaxUpdate]); - yAxisSelection.transition().duration(750).call(buildYAxis(y, yMaxUpdate)); + yAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildYAxis(y, yMaxUpdate)); // Update bars - barsSelection.transition().duration(1000) + barsSelection.transition().duration(TRANSITIONS_DURATION_MS) .attr("x", d => x(timestampToDate(d.time)) - ((INNER_WIDTH / selectedData.length) / 2)) .attr("y", d => y(d.n)) .attr("width", INNER_WIDTH / selectedData.length) .attr("height", d => INNER_HEIGHT - y(d.n)); // Update axis, area and lines position - xAxisSelection.transition().duration(750).call(buildXAxis(x)); + xAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildXAxis(x)); }; // double click reset svg.on("dblclick", () => { x.domain([dates[0], dates[dates.length - 1]]); - xAxisSelection.transition().duration(750).call(buildXAxis(x)); + xAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildXAxis(x)); y.domain([0, yMax]); - yAxisSelection.transition().duration(750).call(buildYAxis(y, yMax)); - barsSelection.transition().duration(1000) + yAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildYAxis(y, yMax)); + barsSelection.transition().duration(TRANSITIONS_DURATION_MS) .attr("x", d => x(timestampToDate(d.time)) - ((INNER_WIDTH / data.length) / 2)) .attr("y", d => y(d.n)) .attr("width", INNER_WIDTH / data.length) diff --git a/client/src/app/components/stardust/statistics/charts/LineChart.tsx b/client/src/app/components/stardust/statistics/charts/LineChart.tsx index 88fe72d06..6a370da63 100644 --- a/client/src/app/components/stardust/statistics/charts/LineChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/LineChart.tsx @@ -12,9 +12,11 @@ import { buildXAxis, buildYAxis, computeDataIncludedInSelection, + computeHalfLineWidth, determineGraphLeftPadding, noDataView, timestampToDate, + TRANSITIONS_DURATION_MS, useChartWrapperSize, useSingleValueTooltip, useTouchMoveEffect @@ -120,10 +122,8 @@ const LineChart: React.FC = ({ chartId, title, info, data, label const attachPathAndCircles = () => { svg.selectAll(".hover-circles").remove(); svg.selectAll(".hover-lines").remove(); - const halfLineWidth = data.length > 1 ? - ((x(timestampToDate(data[1].time)) ?? 0) - (x(timestampToDate(data[0].time)) ?? 0)) / 2 : - 18; + const halfLineWidth = computeHalfLineWidth(data, x); svg.append("g") .attr("class", "hover-circles") .selectAll("g") @@ -185,14 +185,11 @@ const LineChart: React.FC = ({ chartId, title, info, data, label const selectedData = computeDataIncludedInSelection(x, data); const yMaxUpdate = max(selectedData, d => d.n) ?? 1; y.domain([0, yMaxUpdate]); - yAxisSelection.transition().duration(750).call(buildYAxis(y, yMaxUpdate)); + yAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildYAxis(y, yMaxUpdate)); // Update axis, area and lines position - xAxisSelection.transition().duration(1000).call(buildXAxis(x)); - lineSelection - .transition() - .duration(750) - .attr("d", lineGen); + xAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildXAxis(x)); + lineSelection.transition().duration(TRANSITIONS_DURATION_MS).attr("d", lineGen); // rebuild the hover activated lines & cicles attachPathAndCircles(); @@ -203,11 +200,8 @@ const LineChart: React.FC = ({ chartId, title, info, data, label x.domain([dates[0], dates[dates.length - 1]]); xAxisSelection.transition().call(buildXAxis(x)); y.domain([0, yMax]); - yAxisSelection.transition().duration(750).call(buildYAxis(y, yMax)); - lineSelection - .transition() - .duration(500) - .attr("d", lineGen); + yAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildYAxis(y, yMax)); + lineSelection.transition().duration(TRANSITIONS_DURATION_MS).attr("d", lineGen); attachPathAndCircles(); }); } diff --git a/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx b/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx index 2bb121016..fe0d624e0 100644 --- a/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx @@ -16,7 +16,8 @@ import { timestampToDate, buildXAxis, buildYAxis, - computeDataIncludedInSelection + computeDataIncludedInSelection, + TRANSITIONS_DURATION_MS } from "../ChartUtils"; import "./Chart.scss"; @@ -165,16 +166,16 @@ const StackedBarChart: React.FC = ({ }) ); y.domain([0, yMaxUpdate]); - yAxisSelection.transition().duration(750).call(buildYAxis(y, yMaxUpdate)); + yAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildYAxis(y, yMaxUpdate)); // Update bars - barsSelection.transition().duration(1000) + barsSelection.transition().duration(TRANSITIONS_DURATION_MS) .attr("x", d => x(timestampToDate(d.data.time)) - ((INNER_WIDTH / selectedData.length) / 2)) .attr("y", d => y(d[1])) .attr("height", d => y(d[0]) - y(d[1])) .attr("width", INNER_WIDTH / selectedData.length); // Update axis, area and lines position - xAxisSelection.transition().duration(1000).call(buildXAxis(x)); + xAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildXAxis(x)); }; // double click reset @@ -182,8 +183,8 @@ const StackedBarChart: React.FC = ({ x.domain([groups[0], groups[groups.length - 1]]); xAxisSelection.transition().call(buildXAxis(x)); y.domain([0, yMax]); - yAxisSelection.transition().duration(750).call(buildYAxis(y, yMax)); - barsSelection.transition().duration(1000) + yAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildYAxis(y, yMax)); + barsSelection.transition().duration(TRANSITIONS_DURATION_MS) .attr("x", d => x(timestampToDate(d.data.time)) - ((INNER_WIDTH / data.length) / 2)) .attr("y", d => y(d[1])) .attr("height", d => y(d[0]) - y(d[1])) diff --git a/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx b/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx index aae73d2ee..3f93fde19 100644 --- a/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx @@ -11,9 +11,11 @@ import { buildXAxis, buildYAxis, computeDataIncludedInSelection, + computeHalfLineWidth, determineGraphLeftPadding, noDataView, timestampToDate, + TRANSITIONS_DURATION_MS, useChartWrapperSize, useMultiValueTooltip, useTouchMoveEffect @@ -141,10 +143,8 @@ const StackedLineChart: React.FC = ({ const attachOnHoverLinesAndCircles = () => { svg.selectAll(".hover-circles").remove(); svg.selectAll(".hover-lines").remove(); - const halfLineWidth = data.length > 1 ? - ((x(timestampToDate(data[1].time)) ?? 0) - (x(timestampToDate(data[0].time)) ?? 0)) / 2 : - 18; + const halfLineWidth = computeHalfLineWidth(data, x); for (const dataStack of stackedData) { svg.append("g") .attr("class", "hover-circles") @@ -220,17 +220,11 @@ const StackedLineChart: React.FC = ({ const yMaxUpdate = Math.max(...selectedData.map(d => Math.max(...subgroups.map(key => d[key])))); y.domain([0, yMaxUpdate]); // Update axis - yAxisSelection.transition().duration(750).call(buildYAxis(y, yMaxUpdate)); - xAxisSelection.transition().duration(1000).call(buildXAxis(x)); + yAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildYAxis(y, yMaxUpdate)); + xAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildXAxis(x)); // Update area and lines - areaSelection - .transition() - .duration(750) - .attr("d", areaGen); - lineSelection - .transition() - .duration(750) - .attr("d", lineGen); + areaSelection.transition().duration(TRANSITIONS_DURATION_MS).attr("d", areaGen); + lineSelection.transition().duration(TRANSITIONS_DURATION_MS).attr("d", lineGen); // rebuild the hover activated lines & cicles attachOnHoverLinesAndCircles(); }; @@ -240,15 +234,9 @@ const StackedLineChart: React.FC = ({ x.domain([groups[0], groups[groups.length - 1]]); xAxisSelection.transition().call(buildXAxis(x)); y.domain([0, yMax]); - yAxisSelection.transition().duration(750).call(buildYAxis(y, yMax)); - areaSelection - .transition() - .duration(500) - .attr("d", areaGen); - lineSelection - .transition() - .duration(500) - .attr("d", lineGen); + yAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildYAxis(y, yMax)); + areaSelection.transition().duration(TRANSITIONS_DURATION_MS).attr("d", areaGen); + lineSelection.transition().duration(TRANSITIONS_DURATION_MS).attr("d", lineGen); attachOnHoverLinesAndCircles(); }); From bb1e4fcca67af4b0fcb88833343584cf402a5e6f Mon Sep 17 00:00:00 2001 From: Mario Sarcevic Date: Fri, 28 Apr 2023 19:37:18 +0200 Subject: [PATCH 14/14] feat(client/statistics): Prevent infinite brushing on charts + Add some small improvements --- .../stardust/statistics/ChartUtils.tsx | 2 +- .../stardust/statistics/charts/BarChart.tsx | 27 ++++++------ .../stardust/statistics/charts/LineChart.tsx | 26 ++++++------ .../statistics/charts/StackedBarChart.tsx | 42 ++++++++++--------- .../statistics/charts/StackedLineChart.tsx | 26 +++++++----- 5 files changed, 67 insertions(+), 56 deletions(-) diff --git a/client/src/app/components/stardust/statistics/ChartUtils.tsx b/client/src/app/components/stardust/statistics/ChartUtils.tsx index 98646691c..2d421bd6f 100644 --- a/client/src/app/components/stardust/statistics/ChartUtils.tsx +++ b/client/src/app/components/stardust/statistics/ChartUtils.tsx @@ -175,7 +175,7 @@ const tickMultiFormat = (date: Date | NumberValue) => { }; export const buildXAxis: (scale: ScaleTime) => Axis = scale => - axisBottom(scale).tickFormat(tickMultiFormat) as Axis; + axisBottom(scale).ticks(8).tickFormat(tickMultiFormat) as Axis; export const buildYAxis = (scale: ScaleLinear, theYMax: number) => axisLeft(scale.nice()).tickFormat(format(d3FormatSpecifier(theYMax))); diff --git a/client/src/app/components/stardust/statistics/charts/BarChart.tsx b/client/src/app/components/stardust/statistics/charts/BarChart.tsx index 38e756623..ae3f79cab 100644 --- a/client/src/app/components/stardust/statistics/charts/BarChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/BarChart.tsx @@ -133,19 +133,22 @@ const BarChart: React.FC = ({ chartId, title, info, data, label, } const selectedData = computeDataIncludedInSelection(x, data); - const yMaxUpdate = max(selectedData, d => d.n) ?? 1; - y.domain([0, yMaxUpdate]); - yAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildYAxis(y, yMaxUpdate)); - // Update bars - barsSelection.transition().duration(TRANSITIONS_DURATION_MS) - .attr("x", d => x(timestampToDate(d.time)) - ((INNER_WIDTH / selectedData.length) / 2)) - .attr("y", d => y(d.n)) - .attr("width", INNER_WIDTH / selectedData.length) - .attr("height", d => INNER_HEIGHT - y(d.n)); - - // Update axis, area and lines position - xAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildXAxis(x)); + // to prevent infinite brushing + if (selectedData.length > 1) { + const yMaxUpdate = max(selectedData, d => d.n) ?? 1; + y.domain([0, yMaxUpdate]); + // Update axis + xAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildXAxis(x)); + yAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildYAxis(y, yMaxUpdate)); + + // Update bars + barsSelection.transition().duration(TRANSITIONS_DURATION_MS) + .attr("x", d => x(timestampToDate(d.time)) - ((INNER_WIDTH / selectedData.length) / 2)) + .attr("y", d => y(d.n)) + .attr("width", INNER_WIDTH / selectedData.length) + .attr("height", d => INNER_HEIGHT - y(d.n)); + } }; // double click reset diff --git a/client/src/app/components/stardust/statistics/charts/LineChart.tsx b/client/src/app/components/stardust/statistics/charts/LineChart.tsx index 6a370da63..238b26774 100644 --- a/client/src/app/components/stardust/statistics/charts/LineChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/LineChart.tsx @@ -88,7 +88,7 @@ const LineChart: React.FC = ({ chartId, title, info, data, label .append("clipPath") .attr("id", `clip-${chartId}`) .append("rect") - .attr("width", width) + .attr("width", INNER_WIDTH) .attr("height", height) .attr("x", 0) .attr("y", 0); @@ -178,21 +178,21 @@ const LineChart: React.FC = ({ chartId, title, info, data, label brushSelection.call(brush.move, null); } - const from = x.domain()[0]; - from.setHours(0, 0, 0, 0); - const to = x.domain()[1]; - to.setHours(0, 0, 0, 0); const selectedData = computeDataIncludedInSelection(x, data); - const yMaxUpdate = max(selectedData, d => d.n) ?? 1; - y.domain([0, yMaxUpdate]); - yAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildYAxis(y, yMaxUpdate)); - // Update axis, area and lines position - xAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildXAxis(x)); - lineSelection.transition().duration(TRANSITIONS_DURATION_MS).attr("d", lineGen); + // to prevent infinite brushing + if (selectedData.length > 1) { + const yMaxUpdate = max(selectedData, d => d.n) ?? 1; + y.domain([0, yMaxUpdate]); - // rebuild the hover activated lines & cicles - attachPathAndCircles(); + // Update axis and lines position + xAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildXAxis(x)); + yAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildYAxis(y, yMaxUpdate)); + lineSelection.transition().duration(TRANSITIONS_DURATION_MS).attr("d", lineGen); + + // rebuild the hover activated lines & cicles + attachPathAndCircles(); + } }; // double click reset diff --git a/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx b/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx index fe0d624e0..6fb6c0610 100644 --- a/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/StackedBarChart.tsx @@ -156,26 +156,30 @@ const StackedBarChart: React.FC = ({ } const selectedData = computeDataIncludedInSelection(x, data); - const yMaxUpdate = Math.max( - ...selectedData.map(d => { - let sum = 0; - for (const key of subgroups) { - sum += d[key]; - } - return sum; - }) - ); - y.domain([0, yMaxUpdate]); - yAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildYAxis(y, yMaxUpdate)); - // Update bars - barsSelection.transition().duration(TRANSITIONS_DURATION_MS) - .attr("x", d => x(timestampToDate(d.data.time)) - ((INNER_WIDTH / selectedData.length) / 2)) - .attr("y", d => y(d[1])) - .attr("height", d => y(d[0]) - y(d[1])) - .attr("width", INNER_WIDTH / selectedData.length); - // Update axis, area and lines position - xAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildXAxis(x)); + // to prevent infinite brushing + if (selectedData.length > 1) { + const yMaxUpdate = Math.max( + ...selectedData.map(d => { + let sum = 0; + for (const key of subgroups) { + sum += d[key]; + } + return sum; + }) + ); + y.domain([0, yMaxUpdate]); + // Update axis + xAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildXAxis(x)); + yAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildYAxis(y, yMaxUpdate)); + + // Update bars + barsSelection.transition().duration(TRANSITIONS_DURATION_MS) + .attr("x", d => x(timestampToDate(d.data.time)) - ((INNER_WIDTH / selectedData.length) / 2)) + .attr("y", d => y(d[1])) + .attr("height", d => y(d[0]) - y(d[1])) + .attr("width", INNER_WIDTH / selectedData.length); + } }; // double click reset diff --git a/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx b/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx index 3f93fde19..fa05b06cd 100644 --- a/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx +++ b/client/src/app/components/stardust/statistics/charts/StackedLineChart.tsx @@ -100,7 +100,7 @@ const StackedLineChart: React.FC = ({ .append("clipPath") .attr("id", `clip-${chartId}`) .append("rect") - .attr("width", width) + .attr("width", INNER_WIDTH) .attr("height", height) .attr("x", 0) .attr("y", 0); @@ -217,16 +217,20 @@ const StackedLineChart: React.FC = ({ } const selectedData = computeDataIncludedInSelection(x, data); - const yMaxUpdate = Math.max(...selectedData.map(d => Math.max(...subgroups.map(key => d[key])))); - y.domain([0, yMaxUpdate]); - // Update axis - yAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildYAxis(y, yMaxUpdate)); - xAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildXAxis(x)); - // Update area and lines - areaSelection.transition().duration(TRANSITIONS_DURATION_MS).attr("d", areaGen); - lineSelection.transition().duration(TRANSITIONS_DURATION_MS).attr("d", lineGen); - // rebuild the hover activated lines & cicles - attachOnHoverLinesAndCircles(); + + // to prevent infinite brushing + if (selectedData.length > 1) { + const yMaxUpdate = Math.max(...selectedData.map(d => Math.max(...subgroups.map(key => d[key])))); + y.domain([0, yMaxUpdate]); + // Update axis + yAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildYAxis(y, yMaxUpdate)); + xAxisSelection.transition().duration(TRANSITIONS_DURATION_MS).call(buildXAxis(x)); + // Update area and lines + areaSelection.transition().duration(TRANSITIONS_DURATION_MS).attr("d", areaGen); + lineSelection.transition().duration(TRANSITIONS_DURATION_MS).attr("d", lineGen); + // rebuild the hover activated lines & cicles + attachOnHoverLinesAndCircles(); + } }; // double click reset