// Copyright (c) 2020-2021 Drew Lemmy // This file is part of KristWeb 2 under GPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt import React, { useState, useEffect, useMemo } from "react"; import classNames from "classnames"; import { Card, Skeleton, Empty, Row, Col, Tooltip, Select } from "antd"; import { useSelector, shallowEqual } from "react-redux"; import { RootState } from "../../store"; import { useTranslation, Trans } from "react-i18next"; import { Line } from "react-chartjs-2"; import * as api from "../../krist/api"; import { estimateHashRate } from "../../utils/currency"; import { KristConstants } from "../../krist/api/types"; import { trailingThrottleState } from "../../utils/promiseThrottle"; import { SmallResult } from "../../components/results/SmallResult"; import { Statistic } from "../../components/Statistic"; import Debug from "debug"; const debug = Debug("kristweb:block-difficulty-card"); // ============================================================================= // Chart.JS theming options // ============================================================================= const CHART_FILL_COLOUR = "#6495ed33"; const CHART_LINE_COLOUR = "#6495ed"; const CHART_GRID_COLOUR = "#434a6b"; const CHART_FONT_COLOUR = "#8991ab"; const CHART_HEIGHT = 180; const CHART_OPTIONS_BASE = { maintainAspectRatio: false, animation: { duration: 0 }, hover: { animationDuration: 0 }, responsiveAnimationDuration: 0, legend: { display: false }, elements: { line: { tension: 0 } }, tooltips: { mode: "index", intersect: false, displayColors: false, position: "nearest" } }; const CHART_DATASET_OPTIONS = { backgroundColor: CHART_FILL_COLOUR, borderColor: CHART_LINE_COLOUR, borderWidth: 2, pointRadius: 0 }; const CHART_OPTIONS_X_AXIS = { type: "time", bounds: "data", ticks: { maxTicksLimit: 12, fontColor: CHART_FONT_COLOUR, fontSize: 11, padding: 8, lineHeight: 0.5 // Push the tick text to the bottom }, gridLines: { drawBorder: true, drawTicks: true, color: CHART_GRID_COLOUR, tickMarkLength: 1, zeroLineWidth: 1, zeroLineColor: CHART_GRID_COLOUR } }; const CHART_OPTIONS_Y_AXIS = { type: "linear", ticks: { maxTicksLimit: 6, fontColor: CHART_FONT_COLOUR, fontSize: 11, suggestedMin: 100, suggestedMax: 100000, beginAtZero: true, padding: 8 }, gridLines: { drawBorder: true, drawTicks: true, color: CHART_GRID_COLOUR, tickMarkLength: 1, zeroLineWidth: 1, zeroLineColor: CHART_GRID_COLOUR } }; const WORK_THROTTLE = 500; async function _fetchWorkOverTime(constants: KristConstants): Promise<{ x: Date; y: number }[]> { debug("fetching work over time"); const data = await api.get<{ work: number[] }>("work/day"); // Convert the array indices to Dates, based on the fact that the array // should contain one block per secondsPerBlock (typically 1440 elements, // one per minute). This can be passed directly into Chart.JS. return data.work.map((work, i, arr) => ({ x: new Date(Date.now() - ((arr.length - i) * (constants.seconds_per_block * 1000))), y: work })); } export function BlockDifficultyCard(): JSX.Element { const { t } = useTranslation(); const syncNode = api.useSyncNode(); const lastBlockID = useSelector((s: RootState) => s.node.lastBlockID); const work = useSelector((s: RootState) => s.node.detailedWork?.work); const constants = useSelector((s: RootState) => s.node.constants, shallowEqual); const [workOverTime, setWorkOverTime] = useState<{ x: Date; y: number }[] | undefined>(); const [error, setError] = useState<Error | undefined>(); const [loading, setLoading] = useState(true); const [chartMode, setChartMode] = useState<"linear" | "logarithmic">("linear"); const fetchWorkOverTime = useMemo(() => trailingThrottleState(_fetchWorkOverTime, WORK_THROTTLE, false, setWorkOverTime, setError, setLoading), []); // Fetch the new work data whenever the sync node, block ID, or node constants // change. This is usually only going to be triggered by the block ID // changing, which is handled by WebsocketService. useEffect(() => { if (!syncNode) return; fetchWorkOverTime(constants); }, [syncNode, lastBlockID, constants, constants.seconds_per_block, fetchWorkOverTime]); function chart(): JSX.Element { return <Line height={CHART_HEIGHT} data={{ datasets: [{ ...CHART_DATASET_OPTIONS, label: t("dashboard.blockDifficultyChartWork"), data: workOverTime }] }} options={{ ...CHART_OPTIONS_BASE, scales: { xAxes: [CHART_OPTIONS_X_AXIS], yAxes: [{ ...CHART_OPTIONS_Y_AXIS, type: chartMode, ticks: { ...CHART_OPTIONS_Y_AXIS.ticks, suggestedMin: constants.min_work, suggestedMax: constants.max_work } }] } }} />; } /* In its own component to work with i18next Trans */ function HashRate(): JSX.Element { return <span className="difficulty-hash-rate-rate"> {estimateHashRate(work || 0, constants.seconds_per_block)} </span>; } function display(): JSX.Element | null { if (!work || !workOverTime) return null; return <Row> <Col span={24} md={4} className="left-col"> {/* Current work */} <Statistic value={work.toLocaleString()} /> {/* Approximate network hash rate */} <Tooltip title={t("dashboard.blockDifficultyHashRateTooltip")}> <div className="difficulty-hash-rate"> <Trans t={t} i18nKey="dashboard.blockDifficultyHashRate"> Approx. <HashRate /> </Trans> </div> </Tooltip> {/* Spacer to push the dropdown to the bottom */} <div className="spacer" /> {/* Chart Y-axis scale dropdown */} <Select value={chartMode} className="chart-mode-dropdown" onSelect={value => setChartMode(value)} > <Select.Option value="linear">{t("dashboard.blockDifficultyChartLinear")}</Select.Option> <Select.Option value="logarithmic">{t("dashboard.blockDifficultyChartLog")}</Select.Option> </Select> </Col> {/* Work over time chart */} <Col span={24} md={20}>{chart()}</Col> </Row>; } const isEmpty = !loading && error; const classes = classNames("kw-card", "dashboard-card-block-difficulty", { "empty": isEmpty }); return <Card title={t("dashboard.blockDifficultyCardTitle")} className={classes}> <Skeleton paragraph={{ rows: 2 }} title={false} active loading={loading}> {error ? <SmallResult status="error" title={t("error")} subTitle={t("dashboard.blockDifficultyError")} /> : (work && workOverTime ? display() : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} /> )} </Skeleton> </Card>; }