From 8e504880d446487fe41e3c93c472df0fba4f2733 Mon Sep 17 00:00:00 2001 From: DimaDemchenko Date: Thu, 28 Mar 2024 15:40:09 +0200 Subject: [PATCH 01/21] simplified jsx --- .../p2p-media-loader-demo/src/components/GraphNetwork.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/p2p-media-loader-demo/src/components/GraphNetwork.tsx b/packages/p2p-media-loader-demo/src/components/GraphNetwork.tsx index e362cc57..35e8caa6 100644 --- a/packages/p2p-media-loader-demo/src/components/GraphNetwork.tsx +++ b/packages/p2p-media-loader-demo/src/components/GraphNetwork.tsx @@ -43,9 +43,5 @@ export const GraphNetwork = ({ peers }: GraphNetworkProps) => { network.setData(graphData); }, [network, peers]); - return ( - <> -
- - ); + return
; }; From 761ee3a0917d707573659cad27ab46b41df5a142 Mon Sep 17 00:00:00 2001 From: DimaDemchenko Date: Fri, 29 Mar 2024 13:50:46 +0200 Subject: [PATCH 02/21] chart draft with 1 value --- packages/p2p-media-loader-demo/package.json | 5 +- .../src/components/Demo.tsx | 15 + .../src/components/StatsChart.tsx | 127 +++++ pnpm-lock.yaml | 486 ++++++++++++++++++ 4 files changed, 632 insertions(+), 1 deletion(-) create mode 100644 packages/p2p-media-loader-demo/src/components/StatsChart.tsx diff --git a/packages/p2p-media-loader-demo/package.json b/packages/p2p-media-loader-demo/package.json index ad575f26..09a5005c 100644 --- a/packages/p2p-media-loader-demo/package.json +++ b/packages/p2p-media-loader-demo/package.json @@ -44,9 +44,12 @@ "clean-with-modules": "rimraf node_modules && pnpm clean" }, "dependencies": { + "@types/d3": "^7.4.3", + "d3": "^7.9.0", + "hls.js": "^1.5.7", "p2p-media-loader-core": "workspace:*", "p2p-media-loader-hlsjs": "workspace:*", - "hls.js": "^1.5.7", + "rickshaw": "^1.7.1", "vis-network": "^9.1.9" }, "devDependencies": { diff --git a/packages/p2p-media-loader-demo/src/components/Demo.tsx b/packages/p2p-media-loader-demo/src/components/Demo.tsx index 2a7198f2..40a9a71d 100644 --- a/packages/p2p-media-loader-demo/src/components/Demo.tsx +++ b/packages/p2p-media-loader-demo/src/components/Demo.tsx @@ -6,6 +6,7 @@ import { HlsjsPlayer } from "./players/Hlsjs"; import { GraphNetwork } from "./GraphNetwork"; import "./demo.css"; import { useCallback, useState } from "react"; +import { MovingLineChart } from "./StatsChart"; declare global { interface Window { @@ -17,11 +18,24 @@ declare global { export type Player = (typeof PLAYERS)[number]; export const Demo = () => { + //const [data, setData] = useState([]); const { queryParams, setURLQueryParams } = useQueryParams< "player" | "streamUrl" >(); const [peers, setPeers] = useState([]); + /*useEffect(() => { + const interval = setInterval(() => { + const newData: DataItem = { + date: new Date(), + value: Math.random() * 100, + }; + setData((currentData) => [...currentData, newData].slice(-50)); // Keep last 50 data points + }, 1000); + + return () => clearInterval(interval); + }, []);*/ + const onPeerConnect = useCallback((peerId: string) => { setPeers((peers) => { return [...peers, peerId]; @@ -65,6 +79,7 @@ export const Demo = () => { />
+ ; ); }; diff --git a/packages/p2p-media-loader-demo/src/components/StatsChart.tsx b/packages/p2p-media-loader-demo/src/components/StatsChart.tsx new file mode 100644 index 00000000..df162cb1 --- /dev/null +++ b/packages/p2p-media-loader-demo/src/components/StatsChart.tsx @@ -0,0 +1,127 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import React, { useEffect, useRef, useState } from "react"; +import * as d3 from "d3"; + +interface DataItem { + value: number; +} + +export const MovingLineChart = () => { + const [data, setData] = useState(generateInitialData()); + const svgRef = useRef(null); + + useEffect(() => { + const intervalId = setInterval(() => { + setData((currentData) => [ + ...currentData.slice(1), + generateNewDataItem(), + ]); + }, 1000); + return () => clearInterval(intervalId); + }, []); + + useEffect(() => { + if (!svgRef.current) return; + + const margin = { top: 20, right: 30, bottom: 30, left: 50 }; + const width = 710 - margin.left - margin.right; + const height = 250 - margin.top - margin.bottom; + + const xScale = d3 + .scaleLinear() + .domain([0, data.length - 1]) + .range([0, width]); + + const yScale = d3 + .scaleLinear() + .domain([0, d3.max(data, (d) => d.value) ?? 10]) + .range([height, 0]) + .nice(); + + d3.select(svgRef.current).selectAll("*").remove(); + + const svg = d3 + .select(svgRef.current) + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr("transform", `translate(${margin.left},${margin.top})`); + + const area = d3 + .area() + .x((_, i) => xScale(i)) + .y0(height) + .y1((d) => yScale(d.value)) + .curve(d3.curveBasis); + + // Create grid lines for the y-axis + svg + .append("g") + .attr("class", "grid") + // @ts-ignore + .call(d3.axisLeft(yScale).tickSize(-width).tickFormat("")) + .selectAll(".tick line") + .style("stroke", "#ddd") + .style("stroke-dasharray", "2,2"); + + svg + .append("g") + .attr("class", "grid") + .attr("transform", `translate(0,${height})`) + // @ts-ignore + .call(d3.axisBottom(xScale).tickSize(-height).tickFormat("")) + .selectAll(".tick line") + .style("stroke", "#ddd") + .style("stroke-dasharray", "2,2"); + + // Add the area + svg + .append("path") + .datum(data) + .attr("class", "area") + .attr("fill", "steelblue") + .attr("fill-opacity", 0.5) + .attr("d", area); + + svg + .append("path") + .datum(data) + .attr("class", "line") + .attr("fill", "none") + .attr("stroke", "darkblue") + .attr("stroke-width", 1.2) + .attr("d", area); + + // Add the solid y-axis on the left + svg + .append("g") + .call(d3.axisLeft(yScale)) + .select(".domain") + .style("stroke", "#000") + .style("stroke-width", "2"); + + // Add the solid x-axis at the bottom + svg + .append("g") + .attr("transform", `translate(0,${height})`) + // @ts-ignore + .call(d3.axisBottom(xScale).tickFormat("")) + .select(".domain") + .style("stroke", "#000") + .style("stroke-width", "2"); + }, [data]); + + return ; +}; + +function generateInitialData(): DataItem[] { + return Array.from({ length: 60 }, () => ({ + value: Math.random() * 10, + })); +} + +function generateNewDataItem(): DataItem { + return { + value: Math.random() * 10, + }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 683e1a83..9fe5330f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -106,6 +106,12 @@ importers: packages/p2p-media-loader-demo: dependencies: + '@types/d3': + specifier: ^7.4.3 + version: 7.4.3 + d3: + specifier: ^7.9.0 + version: 7.9.0 hls.js: specifier: ^1.5.7 version: 1.5.7 @@ -115,6 +121,9 @@ importers: p2p-media-loader-hlsjs: specifier: workspace:* version: link:../p2p-media-loader-hlsjs + rickshaw: + specifier: ^1.7.1 + version: 1.7.1 vis-network: specifier: ^9.1.9 version: 9.1.9(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.4.0)(uuid@9.0.1)(vis-data@7.1.9)(vis-util@5.0.7) @@ -935,6 +944,185 @@ packages: '@babel/types': 7.24.0 dev: true + /@types/d3-array@3.2.1: + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + dev: false + + /@types/d3-axis@3.0.6: + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + dependencies: + '@types/d3-selection': 3.0.10 + dev: false + + /@types/d3-brush@3.0.6: + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + dependencies: + '@types/d3-selection': 3.0.10 + dev: false + + /@types/d3-chord@3.0.6: + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + dev: false + + /@types/d3-color@3.1.3: + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + dev: false + + /@types/d3-contour@3.0.6: + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + dependencies: + '@types/d3-array': 3.2.1 + '@types/geojson': 7946.0.14 + dev: false + + /@types/d3-delaunay@6.0.4: + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + dev: false + + /@types/d3-dispatch@3.0.6: + resolution: {integrity: sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==} + dev: false + + /@types/d3-drag@3.0.7: + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + dependencies: + '@types/d3-selection': 3.0.10 + dev: false + + /@types/d3-dsv@3.0.7: + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + dev: false + + /@types/d3-ease@3.0.2: + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + dev: false + + /@types/d3-fetch@3.0.7: + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + dependencies: + '@types/d3-dsv': 3.0.7 + dev: false + + /@types/d3-force@3.0.9: + resolution: {integrity: sha512-IKtvyFdb4Q0LWna6ymywQsEYjK/94SGhPrMfEr1TIc5OBeziTi+1jcCvttts8e0UWZIxpasjnQk9MNk/3iS+kA==} + dev: false + + /@types/d3-format@3.0.4: + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + dev: false + + /@types/d3-geo@3.1.0: + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + dependencies: + '@types/geojson': 7946.0.14 + dev: false + + /@types/d3-hierarchy@3.1.7: + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + dev: false + + /@types/d3-interpolate@3.0.4: + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + dependencies: + '@types/d3-color': 3.1.3 + dev: false + + /@types/d3-path@3.1.0: + resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==} + dev: false + + /@types/d3-polygon@3.0.2: + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + dev: false + + /@types/d3-quadtree@3.0.6: + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + dev: false + + /@types/d3-random@3.0.3: + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + dev: false + + /@types/d3-scale-chromatic@3.0.3: + resolution: {integrity: sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==} + dev: false + + /@types/d3-scale@4.0.8: + resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==} + dependencies: + '@types/d3-time': 3.0.3 + dev: false + + /@types/d3-selection@3.0.10: + resolution: {integrity: sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==} + dev: false + + /@types/d3-shape@3.1.6: + resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==} + dependencies: + '@types/d3-path': 3.1.0 + dev: false + + /@types/d3-time-format@4.0.3: + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + dev: false + + /@types/d3-time@3.0.3: + resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==} + dev: false + + /@types/d3-timer@3.0.2: + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + dev: false + + /@types/d3-transition@3.0.8: + resolution: {integrity: sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==} + dependencies: + '@types/d3-selection': 3.0.10 + dev: false + + /@types/d3-zoom@3.0.8: + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.10 + dev: false + + /@types/d3@7.4.3: + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.6 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.9 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.0 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.8 + '@types/d3-scale-chromatic': 3.0.3 + '@types/d3-selection': 3.0.10 + '@types/d3-shape': 3.1.6 + '@types/d3-time': 3.0.3 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.8 + '@types/d3-zoom': 3.0.8 + dev: false + /@types/debug@4.1.12: resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} dependencies: @@ -949,6 +1137,10 @@ packages: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} dev: true + /@types/geojson@7946.0.14: + resolution: {integrity: sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==} + dev: false + /@types/hammerjs@2.0.45: resolution: {integrity: sha512-qkcUlZmX6c4J8q45taBKTL3p+LbITgyx7qhlPYOdOHZB7B31K0mXbP5YA7i7SgDeEGuI9MnumiKPEMrxg8j3KQ==} dev: false @@ -1615,6 +1807,11 @@ packages: delayed-stream: 1.0.0 dev: false + /commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + dev: false + /compact2string@1.4.1: resolution: {integrity: sha512-3D+EY5nsRhqnOwDxveBv5T8wGo4DEvYxjDtPGmdOX+gfr5gE92c2RC0w2wa+xEefm07QuVqqcF3nZJUZ92l/og==} dependencies: @@ -1713,6 +1910,258 @@ packages: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} dev: true + /d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + dependencies: + internmap: 2.0.3 + dev: false + + /d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + dev: false + + /d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + dev: false + + /d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + dependencies: + d3-path: 3.1.0 + dev: false + + /d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + dev: false + + /d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + dev: false + + /d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + dependencies: + delaunator: 5.0.1 + dev: false + + /d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + dev: false + + /d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + dev: false + + /d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + dev: false + + /d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + dev: false + + /d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + dependencies: + d3-dsv: 3.0.1 + dev: false + + /d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + dev: false + + /d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + dev: false + + /d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + dev: false + + /d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + dev: false + + /d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + dependencies: + d3-color: 3.1.0 + dev: false + + /d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + dev: false + + /d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + dev: false + + /d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + dev: false + + /d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + dev: false + + /d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + dev: false + + /d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + dev: false + + /d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + dev: false + + /d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + dependencies: + d3-path: 3.1.0 + dev: false + + /d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + dependencies: + d3-time: 3.1.0 + dev: false + + /d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + dev: false + + /d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + dev: false + + /d3-transition@3.0.1(d3-selection@3.0.0): + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + dev: false + + /d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + dev: false + + /d3@3.5.17: + resolution: {integrity: sha512-yFk/2idb8OHPKkbAL8QaOaqENNoMhIaSHZerk3oQsECwkObkCpJyjYwCe+OHiq6UEdhe1m8ZGARRRO3ljFjlKg==} + dev: false + + /d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.0 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + dev: false + /data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} @@ -1778,6 +2227,12 @@ packages: object-keys: 1.1.1 dev: true + /delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + dependencies: + robust-predicates: 3.0.2 + dev: false + /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -2583,6 +3038,13 @@ packages: resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==} dev: true + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: true @@ -2624,6 +3086,11 @@ packages: side-channel: 1.0.6 dev: true + /internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + dev: false + /ip-address@9.0.5: resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} engines: {node: '>= 12'} @@ -3531,6 +3998,13 @@ packages: engines: {iojs: '>=1.0.0', node: '>=0.10.0'} dev: true + /rickshaw@1.7.1: + resolution: {integrity: sha512-H1eNOST/N8yIJPqdrZjW4R2r4hDN8zbtbBnvm2TzIYZhnfqmQr/fG/f8LMcvDMx1UrIzuBOPH2FTT9t0w79JWA==} + engines: {node: '>= 4.0.0'} + dependencies: + d3: 3.5.17 + dev: false + /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true @@ -3553,6 +4027,10 @@ packages: inherits: 2.0.4 dev: true + /robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + dev: false + /rollup@4.13.0: resolution: {integrity: sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -3585,6 +4063,10 @@ packages: resolution: {integrity: sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g==} dev: false + /rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + dev: false + /safe-array-concat@1.1.2: resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} engines: {node: '>=0.4'} @@ -3612,6 +4094,10 @@ packages: is-regex: 1.1.4 dev: true + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + /scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: From 9b5d0680b9276dc4e27209ec66b75ddfea59d3cf Mon Sep 17 00:00:00 2001 From: DimaDemchenko Date: Fri, 29 Mar 2024 15:09:51 +0200 Subject: [PATCH 03/21] chart draft --- .../src/components/StatsChart.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/p2p-media-loader-demo/src/components/StatsChart.tsx b/packages/p2p-media-loader-demo/src/components/StatsChart.tsx index df162cb1..3fc9258a 100644 --- a/packages/p2p-media-loader-demo/src/components/StatsChart.tsx +++ b/packages/p2p-media-loader-demo/src/components/StatsChart.tsx @@ -32,9 +32,15 @@ export const MovingLineChart = () => { .domain([0, data.length - 1]) .range([0, width]); + const yMaxValue = Math.max(1, d3.max(data, (d) => d.value) ?? 1); + const yTicksCount = + Math.round(yMaxValue / 2) % 2 === 0 + ? Math.round(yMaxValue / 2) + : Math.round(yMaxValue / 2) + 1; + const yScale = d3 .scaleLinear() - .domain([0, d3.max(data, (d) => d.value) ?? 10]) + .domain([0, yMaxValue]) .range([height, 0]) .nice(); @@ -59,7 +65,9 @@ export const MovingLineChart = () => { .append("g") .attr("class", "grid") // @ts-ignore - .call(d3.axisLeft(yScale).tickSize(-width).tickFormat("")) + .call( + d3.axisLeft(yScale).tickSize(-width).ticks(yTicksCount).tickFormat(""), + ) .selectAll(".tick line") .style("stroke", "#ddd") .style("stroke-dasharray", "2,2"); @@ -95,7 +103,7 @@ export const MovingLineChart = () => { // Add the solid y-axis on the left svg .append("g") - .call(d3.axisLeft(yScale)) + .call(d3.axisLeft(yScale).ticks(yTicksCount)) .select(".domain") .style("stroke", "#000") .style("stroke-width", "2"); From bb7047c1249527b4b5fe665f9f97012bad4302f6 Mon Sep 17 00:00:00 2001 From: DimaDemchenko Date: Fri, 29 Mar 2024 16:51:43 +0200 Subject: [PATCH 04/21] draft stacked area chart --- .../src/components/Demo.tsx | 4 +- .../src/components/StatsChart.tsx | 194 +++++++++--------- 2 files changed, 101 insertions(+), 97 deletions(-) diff --git a/packages/p2p-media-loader-demo/src/components/Demo.tsx b/packages/p2p-media-loader-demo/src/components/Demo.tsx index 40a9a71d..50a090eb 100644 --- a/packages/p2p-media-loader-demo/src/components/Demo.tsx +++ b/packages/p2p-media-loader-demo/src/components/Demo.tsx @@ -6,7 +6,7 @@ import { HlsjsPlayer } from "./players/Hlsjs"; import { GraphNetwork } from "./GraphNetwork"; import "./demo.css"; import { useCallback, useState } from "react"; -import { MovingLineChart } from "./StatsChart"; +import { MovingStackedAreaChart } from "./StatsChart"; declare global { interface Window { @@ -79,7 +79,7 @@ export const Demo = () => { />
- ; + ; ); }; diff --git a/packages/p2p-media-loader-demo/src/components/StatsChart.tsx b/packages/p2p-media-loader-demo/src/components/StatsChart.tsx index 3fc9258a..4cba283c 100644 --- a/packages/p2p-media-loader-demo/src/components/StatsChart.tsx +++ b/packages/p2p-media-loader-demo/src/components/StatsChart.tsx @@ -1,20 +1,38 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ +// @ts-nocheck - d3 is not typed + import React, { useEffect, useRef, useState } from "react"; import * as d3 from "d3"; -interface DataItem { - value: number; -} +// Mock data generation for stacked area chart +const generateInitialStackedData = () => { + return Array.from({ length: 120 }, (_, i) => ({ + date: new Date(Date.now() - (19 - i) * 1000 * 60 * 60 * 24), // 20 days of data + series1: Math.random() * 100, + series2: Math.random() * 100, + series3: Math.random() * 100, + })); +}; + +const generateNewStackedDataItem = (data) => { + const lastDate = data[data.length - 1].date; + return { + date: new Date(lastDate.getTime() + 1000 * 60 * 60 * 24), + series1: Math.random() * 100, + series2: Math.random() * 100, + series3: Math.random() * 100, + }; +}; -export const MovingLineChart = () => { - const [data, setData] = useState(generateInitialData()); - const svgRef = useRef(null); +export const MovingStackedAreaChart = () => { + const [data, setData] = useState(generateInitialStackedData()); + const svgRef = useRef(null); useEffect(() => { const intervalId = setInterval(() => { setData((currentData) => [ ...currentData.slice(1), - generateNewDataItem(), + generateNewStackedDataItem(currentData), ]); }, 1000); return () => clearInterval(intervalId); @@ -23,113 +41,99 @@ export const MovingLineChart = () => { useEffect(() => { if (!svgRef.current) return; - const margin = { top: 20, right: 30, bottom: 30, left: 50 }; - const width = 710 - margin.left - margin.right; - const height = 250 - margin.top - margin.bottom; + const margin = { top: 20, right: 20, bottom: 30, left: 50 }, + width = 960 - margin.left - margin.right, + height = 500 - margin.top - margin.bottom; + // Set up the SVG container if it's not already there + const svg = d3.select(svgRef.current); + svg + .selectAll("g") + .data([null]) + .enter() + .append("g") + .attr("transform", `translate(${margin.left},${margin.top})`); + + const content = svg.select("g"); + + // Scales const xScale = d3 - .scaleLinear() - .domain([0, data.length - 1]) + .scaleTime() + .domain(d3.extent(data, (d) => d.date)) .range([0, width]); - const yMaxValue = Math.max(1, d3.max(data, (d) => d.value) ?? 1); - const yTicksCount = - Math.round(yMaxValue / 2) % 2 === 0 - ? Math.round(yMaxValue / 2) - : Math.round(yMaxValue / 2) + 1; - const yScale = d3 .scaleLinear() - .domain([0, yMaxValue]) - .range([height, 0]) - .nice(); + .domain([0, d3.max(data, (d) => d.series1 + d.series2 + d.series3)]) + .range([height, 0]); - d3.select(svgRef.current).selectAll("*").remove(); + // Colors + const color = d3.scaleOrdinal(d3.schemeCategory10); - const svg = d3 - .select(svgRef.current) - .attr("width", width + margin.left + margin.right) - .attr("height", height + margin.top + margin.bottom) - .append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); + // Stack generator + const stack = d3.stack().keys(["series1", "series2", "series3"]); + + const series = stack(data); + // Area generator const area = d3 - .area() - .x((_, i) => xScale(i)) - .y0(height) - .y1((d) => yScale(d.value)) + .area() + .x((d) => xScale(d.data.date)) + .y0((d) => yScale(d[0])) + .y1((d) => yScale(d[1])) .curve(d3.curveBasis); - // Create grid lines for the y-axis - svg - .append("g") - .attr("class", "grid") - // @ts-ignore - .call( - d3.axisLeft(yScale).tickSize(-width).ticks(yTicksCount).tickFormat(""), - ) - .selectAll(".tick line") - .style("stroke", "#ddd") - .style("stroke-dasharray", "2,2"); + // Define the line generator + const line = d3 + .line() + .x((d) => xScale(d.data.date)) + .y((d) => yScale(d[1])) // Use the y1 value for the line's y position + .curve(d3.curveBasis); // Ensure the line is smoothed - svg + // Clear previous areas + content.selectAll(".layer").remove(); + // Axes - re-create axes on update to reflect new data + content.selectAll(".axis").remove(); // Clear previous axes + + content .append("g") - .attr("class", "grid") + .attr("class", "axis axis--x") .attr("transform", `translate(0,${height})`) - // @ts-ignore .call(d3.axisBottom(xScale).tickSize(-height).tickFormat("")) - .selectAll(".tick line") - .style("stroke", "#ddd") - .style("stroke-dasharray", "2,2"); - - // Add the area - svg - .append("path") - .datum(data) - .attr("class", "area") - .attr("fill", "steelblue") - .attr("fill-opacity", 0.5) - .attr("d", area); + .selectAll("line") + .attr("stroke", "#ddd") + .attr("stroke-dasharray", "2,2"); - svg + content + .append("g") + .attr("class", "axis axis--y") + .call(d3.axisLeft(yScale).tickSize(-width)) + .selectAll("line") + .attr("stroke", "#ddd") + .attr("stroke-dasharray", "2,2"); + + // Drawing the layers + content + .selectAll(".layer") + .data(series) + .enter() .append("path") - .datum(data) + .attr("class", "layer") + .attr("d", area) + .style("fill", (d, i) => color(i)) + .style("opacity", 0.7); + + // Update lines + content + .selectAll(".line") + .data(series) + .join("path") .attr("class", "line") - .attr("fill", "none") - .attr("stroke", "darkblue") - .attr("stroke-width", 1.2) - .attr("d", area); - - // Add the solid y-axis on the left - svg - .append("g") - .call(d3.axisLeft(yScale).ticks(yTicksCount)) - .select(".domain") - .style("stroke", "#000") - .style("stroke-width", "2"); - - // Add the solid x-axis at the bottom - svg - .append("g") - .attr("transform", `translate(0,${height})`) - // @ts-ignore - .call(d3.axisBottom(xScale).tickFormat("")) - .select(".domain") - .style("stroke", "#000") - .style("stroke-width", "2"); + .attr("d", line) + .style("fill", "none") + .style("stroke", (d, i) => color(i)) + .style("stroke-width", 1.5); }, [data]); - return ; + return ; }; - -function generateInitialData(): DataItem[] { - return Array.from({ length: 60 }, () => ({ - value: Math.random() * 10, - })); -} - -function generateNewDataItem(): DataItem { - return { - value: Math.random() * 10, - }; -} From 7ead649cd6dcf5e7b1b063e936101452ef94a528 Mon Sep 17 00:00:00 2001 From: DimaDemchenko Date: Sun, 31 Mar 2024 14:32:28 +0300 Subject: [PATCH 05/21] draft of chart with fake data --- .../src/components/StatsChart.tsx | 67 +++++++++++++------ 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/packages/p2p-media-loader-demo/src/components/StatsChart.tsx b/packages/p2p-media-loader-demo/src/components/StatsChart.tsx index 4cba283c..e129e79e 100644 --- a/packages/p2p-media-loader-demo/src/components/StatsChart.tsx +++ b/packages/p2p-media-loader-demo/src/components/StatsChart.tsx @@ -4,13 +4,12 @@ import React, { useEffect, useRef, useState } from "react"; import * as d3 from "d3"; -// Mock data generation for stacked area chart const generateInitialStackedData = () => { return Array.from({ length: 120 }, (_, i) => ({ date: new Date(Date.now() - (19 - i) * 1000 * 60 * 60 * 24), // 20 days of data series1: Math.random() * 100, series2: Math.random() * 100, - series3: Math.random() * 100, + series3: Math.random() * -100, })); }; @@ -42,8 +41,8 @@ export const MovingStackedAreaChart = () => { if (!svgRef.current) return; const margin = { top: 20, right: 20, bottom: 30, left: 50 }, - width = 960 - margin.left - margin.right, - height = 500 - margin.top - margin.bottom; + width = 710 - margin.left - margin.right, + height = 250 - margin.top - margin.bottom; // Set up the SVG container if it's not already there const svg = d3.select(svgRef.current); @@ -56,25 +55,26 @@ export const MovingStackedAreaChart = () => { const content = svg.select("g"); + const series = d3.stack().keys(["series1", "series2"])(data); // Only stack positive series + + const maxStackedValue = d3.max(series, (serie) => + d3.max(serie, (d) => d[1]), + ); + const minSeries3Value = d3.min(data, (d) => d.series3); // Assuming series3 is always negative + // Scales const xScale = d3 .scaleTime() .domain(d3.extent(data, (d) => d.date)) .range([0, width]); - const yScale = d3 .scaleLinear() - .domain([0, d3.max(data, (d) => d.series1 + d.series2 + d.series3)]) + .domain([minSeries3Value, maxStackedValue]) .range([height, 0]); // Colors const color = d3.scaleOrdinal(d3.schemeCategory10); - // Stack generator - const stack = d3.stack().keys(["series1", "series2", "series3"]); - - const series = stack(data); - // Area generator const area = d3 .area() @@ -83,6 +83,13 @@ export const MovingStackedAreaChart = () => { .y1((d) => yScale(d[1])) .curve(d3.curveBasis); + const areaSeries3 = d3 + .area() + .x((d) => xScale(d.date)) + .y0(() => yScale(0)) // This now represents the starting point at the Y-axis line + .y1((d) => yScale(d.series3)) // Extend down to represent the negative value + .curve(d3.curveBasis); // Keeping the curve smooth + // Define the line generator const line = d3 .line() @@ -90,11 +97,15 @@ export const MovingStackedAreaChart = () => { .y((d) => yScale(d[1])) // Use the y1 value for the line's y position .curve(d3.curveBasis); // Ensure the line is smoothed - // Clear previous areas - content.selectAll(".layer").remove(); - // Axes - re-create axes on update to reflect new data - content.selectAll(".axis").remove(); // Clear previous axes + const lineSeries3 = d3 + .line() + .x((d) => xScale(d.date)) + .y((d) => yScale(d.series3)) + .curve(d3.curveBasis); // Match the curve style of the area + // First, draw the axes (make sure this comes first) + content.selectAll(".axis").remove(); // Clear previous axes, if any + // X-axis content .append("g") .attr("class", "axis axis--x") @@ -103,7 +114,7 @@ export const MovingStackedAreaChart = () => { .selectAll("line") .attr("stroke", "#ddd") .attr("stroke-dasharray", "2,2"); - + // Y-axis content .append("g") .attr("class", "axis axis--y") @@ -112,7 +123,8 @@ export const MovingStackedAreaChart = () => { .attr("stroke", "#ddd") .attr("stroke-dasharray", "2,2"); - // Drawing the layers + // Clear previous areas + content.selectAll(".layer").remove(); content .selectAll(".layer") .data(series) @@ -122,8 +134,6 @@ export const MovingStackedAreaChart = () => { .attr("d", area) .style("fill", (d, i) => color(i)) .style("opacity", 0.7); - - // Update lines content .selectAll(".line") .data(series) @@ -133,7 +143,24 @@ export const MovingStackedAreaChart = () => { .style("fill", "none") .style("stroke", (d, i) => color(i)) .style("stroke-width", 1.5); + content + .selectAll(".series3-area") + .data([data]) // Ensure data is wrapped in an array for D3's area generator + .join("path") + .attr("class", "series3-area") + .attr("d", areaSeries3) + .style("fill", "lightblue") // Choose a distinctive fill color for series3 + .style("opacity", 0.7); + content + .selectAll(".series3-line") + .data([data]) + .join("path") + .attr("class", "series3-line") + .attr("d", lineSeries3) + .style("fill", "none") + .style("stroke", "blue") // Ensure it's visually distinct + .style("stroke-width", 2); }, [data]); - return ; + return ; }; From 4b0d93c233d23b03c3ccb4a29c11017891314164 Mon Sep 17 00:00:00 2001 From: DimaDemchenko Date: Sun, 31 Mar 2024 15:45:32 +0300 Subject: [PATCH 06/21] implemented chart with fake data --- .../src/components/StatsChart.tsx | 103 +++++++++--------- 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/packages/p2p-media-loader-demo/src/components/StatsChart.tsx b/packages/p2p-media-loader-demo/src/components/StatsChart.tsx index e129e79e..f15cf4c3 100644 --- a/packages/p2p-media-loader-demo/src/components/StatsChart.tsx +++ b/packages/p2p-media-loader-demo/src/components/StatsChart.tsx @@ -5,24 +5,29 @@ import React, { useEffect, useRef, useState } from "react"; import * as d3 from "d3"; const generateInitialStackedData = () => { + const nowInSeconds = Math.floor(Date.now() / 1000); return Array.from({ length: 120 }, (_, i) => ({ - date: new Date(Date.now() - (19 - i) * 1000 * 60 * 60 * 24), // 20 days of data - series1: Math.random() * 100, - series2: Math.random() * 100, - series3: Math.random() * -100, + date: nowInSeconds - (120 - i), + series1: 0, + series2: 0, + series3: 0, })); }; const generateNewStackedDataItem = (data) => { - const lastDate = data[data.length - 1].date; + const lastSecond = data[data.length - 1].date; return { - date: new Date(lastDate.getTime() + 1000 * 60 * 60 * 24), - series1: Math.random() * 100, - series2: Math.random() * 100, - series3: Math.random() * 100, + date: lastSecond + 1, + series1: Math.random() * 10, + series2: Math.random() * 10, + series3: Math.random() * -10, }; }; +const margin = { top: 20, right: 20, bottom: 30, left: 50 }, + width = 710 - margin.left - margin.right, + height = 250 - margin.top - margin.bottom; + export const MovingStackedAreaChart = () => { const [data, setData] = useState(generateInitialStackedData()); const svgRef = useRef(null); @@ -40,11 +45,6 @@ export const MovingStackedAreaChart = () => { useEffect(() => { if (!svgRef.current) return; - const margin = { top: 20, right: 20, bottom: 30, left: 50 }, - width = 710 - margin.left - margin.right, - height = 250 - margin.top - margin.bottom; - - // Set up the SVG container if it's not already there const svg = d3.select(svgRef.current); svg .selectAll("g") @@ -55,16 +55,16 @@ export const MovingStackedAreaChart = () => { const content = svg.select("g"); - const series = d3.stack().keys(["series1", "series2"])(data); // Only stack positive series + const series = d3.stack().keys(["series1", "series2"])(data); const maxStackedValue = d3.max(series, (serie) => d3.max(serie, (d) => d[1]), ); - const minSeries3Value = d3.min(data, (d) => d.series3); // Assuming series3 is always negative + const minSeries3Value = d3.min(data, (d) => d.series3); + const yTickValues = Math.round((maxStackedValue - minSeries3Value) / 5); - // Scales const xScale = d3 - .scaleTime() + .scaleLinear() .domain(d3.extent(data, (d) => d.date)) .range([0, width]); const yScale = d3 @@ -72,10 +72,8 @@ export const MovingStackedAreaChart = () => { .domain([minSeries3Value, maxStackedValue]) .range([height, 0]); - // Colors const color = d3.scaleOrdinal(d3.schemeCategory10); - // Area generator const area = d3 .area() .x((d) => xScale(d.data.date)) @@ -86,26 +84,24 @@ export const MovingStackedAreaChart = () => { const areaSeries3 = d3 .area() .x((d) => xScale(d.date)) - .y0(() => yScale(0)) // This now represents the starting point at the Y-axis line - .y1((d) => yScale(d.series3)) // Extend down to represent the negative value - .curve(d3.curveBasis); // Keeping the curve smooth + .y0(() => yScale(0)) + .y1((d) => yScale(d.series3)) + .curve(d3.curveBasis); - // Define the line generator const line = d3 .line() .x((d) => xScale(d.data.date)) - .y((d) => yScale(d[1])) // Use the y1 value for the line's y position - .curve(d3.curveBasis); // Ensure the line is smoothed + .y((d) => yScale(d[1])) + .curve(d3.curveBasis); const lineSeries3 = d3 .line() .x((d) => xScale(d.date)) .y((d) => yScale(d.series3)) - .curve(d3.curveBasis); // Match the curve style of the area + .curve(d3.curveBasis); - // First, draw the axes (make sure this comes first) - content.selectAll(".axis").remove(); // Clear previous axes, if any - // X-axis + content.selectAll(".axis").remove(); + // Axes content .append("g") .attr("class", "axis axis--x") @@ -114,17 +110,37 @@ export const MovingStackedAreaChart = () => { .selectAll("line") .attr("stroke", "#ddd") .attr("stroke-dasharray", "2,2"); - // Y-axis + + console.log("yTickValues", yTickValues); content .append("g") .attr("class", "axis axis--y") - .call(d3.axisLeft(yScale).tickSize(-width)) + .call(d3.axisLeft(yScale).tickSize(-width).ticks(yTickValues)) .selectAll("line") .attr("stroke", "#ddd") .attr("stroke-dasharray", "2,2"); - // Clear previous areas + // Data layers content.selectAll(".layer").remove(); + + content + .selectAll(".series3-area") + .data([data]) + .join("path") + .attr("class", "series3-area") + .attr("d", areaSeries3) + .style("fill", "lightblue") + .style("opacity", 0.7); + content + .selectAll(".series3-line") + .data([data]) + .join("path") + .attr("class", "series3-line") + .attr("d", lineSeries3) + .style("fill", "none") + .style("stroke", "blue") + .style("stroke-width", 2); + content .selectAll(".layer") .data(series) @@ -143,23 +159,10 @@ export const MovingStackedAreaChart = () => { .style("fill", "none") .style("stroke", (d, i) => color(i)) .style("stroke-width", 1.5); - content - .selectAll(".series3-area") - .data([data]) // Ensure data is wrapped in an array for D3's area generator - .join("path") - .attr("class", "series3-area") - .attr("d", areaSeries3) - .style("fill", "lightblue") // Choose a distinctive fill color for series3 - .style("opacity", 0.7); - content - .selectAll(".series3-line") - .data([data]) - .join("path") - .attr("class", "series3-line") - .attr("d", lineSeries3) - .style("fill", "none") - .style("stroke", "blue") // Ensure it's visually distinct - .style("stroke-width", 2); + + return () => { + svg.selectAll("*").remove(); + }; }, [data]); return ; From 17f6d959bbc0ffd20e02af3fb429d10f1c72db08 Mon Sep 17 00:00:00 2001 From: DimaDemchenko Date: Mon, 1 Apr 2024 13:20:17 +0300 Subject: [PATCH 07/21] chart with real stat --- .../src/components/Demo.tsx | 48 ++++++++---- .../src/components/StatsChart.tsx | 77 +++++++++++++------ .../src/components/chart.css | 6 ++ .../src/components/players/Hlsjs.tsx | 18 ++++- 4 files changed, 111 insertions(+), 38 deletions(-) create mode 100644 packages/p2p-media-loader-demo/src/components/chart.css diff --git a/packages/p2p-media-loader-demo/src/components/Demo.tsx b/packages/p2p-media-loader-demo/src/components/Demo.tsx index 50a090eb..395f90aa 100644 --- a/packages/p2p-media-loader-demo/src/components/Demo.tsx +++ b/packages/p2p-media-loader-demo/src/components/Demo.tsx @@ -5,7 +5,7 @@ import { useQueryParams } from "../hooks/useQueryParams"; import { HlsjsPlayer } from "./players/Hlsjs"; import { GraphNetwork } from "./GraphNetwork"; import "./demo.css"; -import { useCallback, useState } from "react"; +import { useCallback, useRef, useState } from "react"; import { MovingStackedAreaChart } from "./StatsChart"; declare global { @@ -15,26 +15,46 @@ declare global { } } +const convertToMB = (bytes: number) => bytes / 1024 / 1024; + +type DownloadStats = { + series1: number; + series2: number; + series3: number; +}; + export type Player = (typeof PLAYERS)[number]; export const Demo = () => { - //const [data, setData] = useState([]); + const data = useRef({ + series1: 0, + series2: 0, + series3: 0, + }); const { queryParams, setURLQueryParams } = useQueryParams< "player" | "streamUrl" >(); const [peers, setPeers] = useState([]); - /*useEffect(() => { - const interval = setInterval(() => { - const newData: DataItem = { - date: new Date(), - value: Math.random() * 100, - }; - setData((currentData) => [...currentData, newData].slice(-50)); // Keep last 50 data points - }, 1000); + const onChunkDownloaded = useCallback( + (bytesLength: number, downloadSource: string) => { + switch (downloadSource) { + case "http": + data.current.series1 += convertToMB(bytesLength); + break; + case "p2p": + data.current.series2 += convertToMB(bytesLength); + break; + default: + break; + } + }, + [], + ); - return () => clearInterval(interval); - }, []);*/ + const onChunkUploaded = useCallback((bytesLength: number) => { + data.current.series3 += convertToMB(bytesLength); + }, []); const onPeerConnect = useCallback((peerId: string) => { setPeers((peers) => { @@ -61,6 +81,8 @@ export const Demo = () => { streamUrl={queryParams.streamUrl} onPeerConnect={onPeerConnect} onPeerDisconnect={onPeerDisconnect} + onChunkDownloaded={onChunkDownloaded} + onChunkUploaded={onChunkUploaded} /> ); default: @@ -79,7 +101,7 @@ export const Demo = () => { /> - ; + ); }; diff --git a/packages/p2p-media-loader-demo/src/components/StatsChart.tsx b/packages/p2p-media-loader-demo/src/components/StatsChart.tsx index f15cf4c3..f34d2d00 100644 --- a/packages/p2p-media-loader-demo/src/components/StatsChart.tsx +++ b/packages/p2p-media-loader-demo/src/components/StatsChart.tsx @@ -3,6 +3,22 @@ import React, { useEffect, useRef, useState } from "react"; import * as d3 from "d3"; +import { DownloadStats } from "./Demo"; + +const margin = { top: 20, right: 20, bottom: 30, left: 50 }, + width = 710 - margin.left - margin.right, + height = 250 - margin.top - margin.bottom; + +type StatsChartProps = { + downloadStatsRef: React.RefObject; +}; + +type ChartsData = { + date: number; + series1: number; + series2: number; + series3: number; +}; const generateInitialStackedData = () => { const nowInSeconds = Math.floor(Date.now() / 1000); @@ -14,33 +30,47 @@ const generateInitialStackedData = () => { })); }; -const generateNewStackedDataItem = (data) => { - const lastSecond = data[data.length - 1].date; - return { - date: lastSecond + 1, - series1: Math.random() * 10, - series2: Math.random() * 10, - series3: Math.random() * -10, - }; -}; - -const margin = { top: 20, right: 20, bottom: 30, left: 50 }, - width = 710 - margin.left - margin.right, - height = 250 - margin.top - margin.bottom; - -export const MovingStackedAreaChart = () => { - const [data, setData] = useState(generateInitialStackedData()); +export const MovingStackedAreaChart = ({ + downloadStatsRef, +}: StatsChartProps) => { + const [data, setData] = useState(generateInitialStackedData()); + const [storedData, setStoredData] = useState>({ + series1: 0, + series2: 0, + series3: 0, + }); const svgRef = useRef(null); useEffect(() => { - const intervalId = setInterval(() => { - setData((currentData) => [ - ...currentData.slice(1), - generateNewStackedDataItem(currentData), - ]); + const intervalID = setInterval(() => { + if (!downloadStatsRef.current) return; + + const { series1, series2, series3 } = downloadStatsRef.current; + setData((prevData) => { + const newData = { + date: Math.floor(Date.now() / 1000), + series1: series1, + series2: series2, + series3: series3 * -1, + }; + return [...prevData.slice(1), newData]; + }); + + setStoredData((prevData) => ({ + series1: prevData.series1 + series1, + series2: prevData.series2 + series2, + series3: prevData.series3 + series3, + })); + + downloadStatsRef.current = { + series1: 0, + series2: 0, + series3: 0, + }; }, 1000); - return () => clearInterval(intervalId); - }, []); + + return () => clearInterval(intervalID); + }, [downloadStatsRef]); useEffect(() => { if (!svgRef.current) return; @@ -111,7 +141,6 @@ export const MovingStackedAreaChart = () => { .attr("stroke", "#ddd") .attr("stroke-dasharray", "2,2"); - console.log("yTickValues", yTickValues); content .append("g") .attr("class", "axis axis--y") diff --git a/packages/p2p-media-loader-demo/src/components/chart.css b/packages/p2p-media-loader-demo/src/components/chart.css new file mode 100644 index 00000000..9e23584a --- /dev/null +++ b/packages/p2p-media-loader-demo/src/components/chart.css @@ -0,0 +1,6 @@ +.chart-legend { + position: absolute; + display: flex; + justify-content: center; + margin-top: 10px; +} diff --git a/packages/p2p-media-loader-demo/src/components/players/Hlsjs.tsx b/packages/p2p-media-loader-demo/src/components/players/Hlsjs.tsx index ff3302c7..b3bea402 100644 --- a/packages/p2p-media-loader-demo/src/components/players/Hlsjs.tsx +++ b/packages/p2p-media-loader-demo/src/components/players/Hlsjs.tsx @@ -6,6 +6,8 @@ type HlsjsPlayerProps = { streamUrl: string; onPeerConnect?: (peerId: string) => void; onPeerDisconnect?: (peerId: string) => void; + onChunkDownloaded?: (bytesLength: number, downloadSource: string) => void; + onChunkUploaded?: (bytesLength: number) => void; }; const HlsWithP2P = HlsJsP2PEngine.injectMixin(Hls); @@ -13,6 +15,8 @@ export const HlsjsPlayer = ({ streamUrl, onPeerConnect, onPeerDisconnect, + onChunkDownloaded, + onChunkUploaded, }: HlsjsPlayerProps) => { const videoRef = useRef(null); @@ -26,12 +30,24 @@ export const HlsjsPlayer = ({ if (onPeerDisconnect) { hls.p2pEngine.addEventListener("onPeerClose", onPeerDisconnect); } + if (onChunkDownloaded) { + hls.p2pEngine.addEventListener("onChunkDownloaded", onChunkDownloaded); + } + if (onChunkUploaded) { + hls.p2pEngine.addEventListener("onChunkUploaded", onChunkUploaded); + } hls.attachMedia(videoRef.current); hls.loadSource(streamUrl); return () => hls.destroy(); - }, [onPeerConnect, onPeerDisconnect, streamUrl]); + }, [ + onPeerConnect, + onPeerDisconnect, + onChunkDownloaded, + onChunkUploaded, + streamUrl, + ]); return (
From e24cb93f06da668a4f5854fefd44b08499f36710 Mon Sep 17 00:00:00 2001 From: DimaDemchenko Date: Mon, 1 Apr 2024 15:32:39 +0300 Subject: [PATCH 08/21] added legend with stored data --- .../src/components/StatsChart.tsx | 35 ++++++++++++++-- .../src/components/chart.css | 41 ++++++++++++++++++- 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/packages/p2p-media-loader-demo/src/components/StatsChart.tsx b/packages/p2p-media-loader-demo/src/components/StatsChart.tsx index f34d2d00..7e5dae2c 100644 --- a/packages/p2p-media-loader-demo/src/components/StatsChart.tsx +++ b/packages/p2p-media-loader-demo/src/components/StatsChart.tsx @@ -4,10 +4,11 @@ import React, { useEffect, useRef, useState } from "react"; import * as d3 from "d3"; import { DownloadStats } from "./Demo"; +import "./chart.css"; const margin = { top: 20, right: 20, bottom: 30, left: 50 }, width = 710 - margin.left - margin.right, - height = 250 - margin.top - margin.bottom; + height = 310 - margin.top - margin.bottom; type StatsChartProps = { downloadStatsRef: React.RefObject; @@ -102,7 +103,10 @@ export const MovingStackedAreaChart = ({ .domain([minSeries3Value, maxStackedValue]) .range([height, 0]); - const color = d3.scaleOrdinal(d3.schemeCategory10); + const color = d3 + .scaleOrdinal() + .domain(["series1", "series2"]) + .range(["#faf21b", "#ff7f0e"]); const area = d3 .area() @@ -194,5 +198,30 @@ export const MovingStackedAreaChart = ({ }; }, [data]); - return ; + return ( +
+
+
+
+

+ Download - {(storedData.series1 + storedData.series2).toFixed(2)}{" "} + Mbps +

+
+
+
+

- HTTP - {storedData.series1.toFixed(2)} Mbps

+
+
+
+

- P2P - {storedData.series2.toFixed(2)} Mbps

+
+
+
+

Upload P2P - {storedData.series3.toFixed(2)} Mbps

+
+
+ +
+ ); }; diff --git a/packages/p2p-media-loader-demo/src/components/chart.css b/packages/p2p-media-loader-demo/src/components/chart.css index 9e23584a..bc0140b7 100644 --- a/packages/p2p-media-loader-demo/src/components/chart.css +++ b/packages/p2p-media-loader-demo/src/components/chart.css @@ -1,6 +1,43 @@ .chart-legend { position: absolute; + margin-top: 30px; + margin-left: 70px; + height: 70px; + font-family: Arial; + font-size: 12px; + color: #fff; + background: #404040; + display: inline-block; + padding: 12px 5px; + border-radius: 2px; + opacity: 0.8; +} + +.chart-legend p { + margin: 0; + color: #fff; +} + +.chart-legend .line { + margin-right: 3px; + margin-left: 4px; + border-radius: 2px; + clear: both; + line-height: 140%; + padding-right: 15px; display: flex; - justify-content: center; - margin-top: 10px; + align-items: center; +} + +.chart-legend .swatch { + display: inline-block; + width: 8px; + height: 8px; + border: 1px solid rgba(0, 0, 0, 0.2); +} + +.chart-legend .line .swatch { + display: inline-block; + margin-right: 3px; + border-radius: 2px; } From 6ad141201fd00160c04dc08a18b5e25235962137 Mon Sep 17 00:00:00 2001 From: DimaDemchenko Date: Mon, 1 Apr 2024 16:34:38 +0300 Subject: [PATCH 09/21] colors moved to constants --- .../src/components/StatsChart.tsx | 29 ++++++++++++------- .../p2p-media-loader-demo/src/constants.ts | 6 ++++ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/p2p-media-loader-demo/src/components/StatsChart.tsx b/packages/p2p-media-loader-demo/src/components/StatsChart.tsx index 7e5dae2c..8ac36696 100644 --- a/packages/p2p-media-loader-demo/src/components/StatsChart.tsx +++ b/packages/p2p-media-loader-demo/src/components/StatsChart.tsx @@ -4,6 +4,7 @@ import React, { useEffect, useRef, useState } from "react"; import * as d3 from "d3"; import { DownloadStats } from "./Demo"; +import { COLORS } from "../constants"; import "./chart.css"; const margin = { top: 20, right: 20, bottom: 30, left: 50 }, @@ -22,7 +23,7 @@ type ChartsData = { }; const generateInitialStackedData = () => { - const nowInSeconds = Math.floor(Date.now() / 1000); + const nowInSeconds = Math.floor(performance.now() / 1000); return Array.from({ length: 120 }, (_, i) => ({ date: nowInSeconds - (120 - i), series1: 0, @@ -49,7 +50,7 @@ export const MovingStackedAreaChart = ({ const { series1, series2, series3 } = downloadStatsRef.current; setData((prevData) => { const newData = { - date: Math.floor(Date.now() / 1000), + date: Math.floor(performance.now() / 1000), series1: series1, series2: series2, series3: series3 * -1, @@ -92,7 +93,6 @@ export const MovingStackedAreaChart = ({ d3.max(serie, (d) => d[1]), ); const minSeries3Value = d3.min(data, (d) => d.series3); - const yTickValues = Math.round((maxStackedValue - minSeries3Value) / 5); const xScale = d3 .scaleLinear() @@ -106,7 +106,7 @@ export const MovingStackedAreaChart = ({ const color = d3 .scaleOrdinal() .domain(["series1", "series2"]) - .range(["#faf21b", "#ff7f0e"]); + .range([COLORS.yellow, COLORS.lightOrange]); const area = d3 .area() @@ -148,7 +148,7 @@ export const MovingStackedAreaChart = ({ content .append("g") .attr("class", "axis axis--y") - .call(d3.axisLeft(yScale).tickSize(-width).ticks(yTickValues)) + .call(d3.axisLeft(yScale).tickSize(-width).ticks(5)) .selectAll("line") .attr("stroke", "#ddd") .attr("stroke-dasharray", "2,2"); @@ -162,7 +162,7 @@ export const MovingStackedAreaChart = ({ .join("path") .attr("class", "series3-area") .attr("d", areaSeries3) - .style("fill", "lightblue") + .style("fill", COLORS.lightBlue) .style("opacity", 0.7); content .selectAll(".series3-line") @@ -202,22 +202,31 @@ export const MovingStackedAreaChart = ({
-
+

Download - {(storedData.series1 + storedData.series2).toFixed(2)}{" "} Mbps

-
+

- HTTP - {storedData.series1.toFixed(2)} Mbps

-
+

- P2P - {storedData.series2.toFixed(2)} Mbps

-
+

Upload P2P - {storedData.series3.toFixed(2)} Mbps

diff --git a/packages/p2p-media-loader-demo/src/constants.ts b/packages/p2p-media-loader-demo/src/constants.ts index ee336592..7ef5950f 100644 --- a/packages/p2p-media-loader-demo/src/constants.ts +++ b/packages/p2p-media-loader-demo/src/constants.ts @@ -20,3 +20,9 @@ export const DEFAULT_GRAPH_DATA = { nodes: [{ id: 1, label: "You", color: "#5390e0" }], edges: [], }; +export const COLORS = { + yellow: "#faf21b", + lightOrange: "#ff7f0e", + lightBlue: "#ADD8E6", + torchRed: "#ff1745", +}; From d408ab86f5ab788568b479fdb67cae31aa100831 Mon Sep 17 00:00:00 2001 From: DimaDemchenko Date: Mon, 1 Apr 2024 16:54:17 +0300 Subject: [PATCH 10/21] added percentage calculation --- .../src/components/StatsChart.tsx | 53 ++++++++++++++----- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/packages/p2p-media-loader-demo/src/components/StatsChart.tsx b/packages/p2p-media-loader-demo/src/components/StatsChart.tsx index 8ac36696..d15dc0f7 100644 --- a/packages/p2p-media-loader-demo/src/components/StatsChart.tsx +++ b/packages/p2p-media-loader-demo/src/components/StatsChart.tsx @@ -32,14 +32,22 @@ const generateInitialStackedData = () => { })); }; +type StoredData = { + totalDownloaded: number; + httpDownloaded: number; + p2pDownloaded: number; + p2pUploaded: number; +}; + export const MovingStackedAreaChart = ({ downloadStatsRef, }: StatsChartProps) => { const [data, setData] = useState(generateInitialStackedData()); - const [storedData, setStoredData] = useState>({ - series1: 0, - series2: 0, - series3: 0, + const [storedData, setStoredData] = useState({ + totalDownloaded: 0, + httpDownloaded: 0, + p2pDownloaded: 0, + p2pUploaded: 0, }); const svgRef = useRef(null); @@ -48,6 +56,7 @@ export const MovingStackedAreaChart = ({ if (!downloadStatsRef.current) return; const { series1, series2, series3 } = downloadStatsRef.current; + setData((prevData) => { const newData = { date: Math.floor(performance.now() / 1000), @@ -59,9 +68,10 @@ export const MovingStackedAreaChart = ({ }); setStoredData((prevData) => ({ - series1: prevData.series1 + series1, - series2: prevData.series2 + series2, - series3: prevData.series3 + series3, + totalDownloaded: prevData.totalDownloaded + series1 + series2, + httpDownloaded: prevData.httpDownloaded + series1, + p2pDownloaded: prevData.p2pDownloaded + series2, + p2pUploaded: prevData.p2pUploaded + series3, })); downloadStatsRef.current = { @@ -206,28 +216,43 @@ export const MovingStackedAreaChart = ({ className="swatch" style={{ backgroundColor: COLORS.torchRed }} /> -

- Download - {(storedData.series1 + storedData.series2).toFixed(2)}{" "} - Mbps -

+

Download - {storedData.totalDownloaded.toFixed(2)} Mbps

-

- HTTP - {storedData.series1.toFixed(2)} Mbps

+

+ {" "} + - HTTP - {storedData.httpDownloaded.toFixed(2)} Mbps -{" "} + {( + (storedData.httpDownloaded / storedData.totalDownloaded) * + 100 + ).toFixed(2)} + % +

-

- P2P - {storedData.series2.toFixed(2)} Mbps

+

+ {" "} + - P2P - {storedData.p2pDownloaded.toFixed(2)} Mbps -{" "} + {storedData.p2pDownloaded + ? ( + (storedData.p2pDownloaded / storedData.totalDownloaded) * + 100 + ).toFixed(2) + : 0} + % +

-

Upload P2P - {storedData.series3.toFixed(2)} Mbps

+

Upload P2P - {storedData.p2pUploaded.toFixed(2)} Mbps

From 5cf0c2e4dade8286cab5fdbac32fb288323611c9 Mon Sep 17 00:00:00 2001 From: DimaDemchenko Date: Mon, 1 Apr 2024 17:34:04 +0300 Subject: [PATCH 11/21] Chart legend moved to separate component --- .../src/components/Demo.tsx | 2 +- .../src/components/chart/ChartLegend.tsx | 21 +++++ .../src/components/{ => chart}/StatsChart.tsx | 78 ++++++++----------- .../src/components/{ => chart}/chart.css | 0 4 files changed, 53 insertions(+), 48 deletions(-) create mode 100644 packages/p2p-media-loader-demo/src/components/chart/ChartLegend.tsx rename packages/p2p-media-loader-demo/src/components/{ => chart}/StatsChart.tsx (78%) rename packages/p2p-media-loader-demo/src/components/{ => chart}/chart.css (100%) diff --git a/packages/p2p-media-loader-demo/src/components/Demo.tsx b/packages/p2p-media-loader-demo/src/components/Demo.tsx index 395f90aa..c3136943 100644 --- a/packages/p2p-media-loader-demo/src/components/Demo.tsx +++ b/packages/p2p-media-loader-demo/src/components/Demo.tsx @@ -6,7 +6,7 @@ import { HlsjsPlayer } from "./players/Hlsjs"; import { GraphNetwork } from "./GraphNetwork"; import "./demo.css"; import { useCallback, useRef, useState } from "react"; -import { MovingStackedAreaChart } from "./StatsChart"; +import { MovingStackedAreaChart } from "./chart/StatsChart"; declare global { interface Window { diff --git a/packages/p2p-media-loader-demo/src/components/chart/ChartLegend.tsx b/packages/p2p-media-loader-demo/src/components/chart/ChartLegend.tsx new file mode 100644 index 00000000..f49d1128 --- /dev/null +++ b/packages/p2p-media-loader-demo/src/components/chart/ChartLegend.tsx @@ -0,0 +1,21 @@ +type LegendItem = { + color: string; + content: string | JSX.Element; +}; + +type ChartLegendProps = { + legendItems: LegendItem[]; +}; + +export const ChartLegend = ({ legendItems }: ChartLegendProps) => { + return ( +
+ {legendItems.map((item, index) => ( +
+
+

{item.content}

+
+ ))} +
+ ); +}; diff --git a/packages/p2p-media-loader-demo/src/components/StatsChart.tsx b/packages/p2p-media-loader-demo/src/components/chart/StatsChart.tsx similarity index 78% rename from packages/p2p-media-loader-demo/src/components/StatsChart.tsx rename to packages/p2p-media-loader-demo/src/components/chart/StatsChart.tsx index d15dc0f7..ead630cc 100644 --- a/packages/p2p-media-loader-demo/src/components/StatsChart.tsx +++ b/packages/p2p-media-loader-demo/src/components/chart/StatsChart.tsx @@ -3,9 +3,10 @@ import React, { useEffect, useRef, useState } from "react"; import * as d3 from "d3"; -import { DownloadStats } from "./Demo"; -import { COLORS } from "../constants"; +import { DownloadStats } from "../Demo"; +import { COLORS } from "../../constants"; import "./chart.css"; +import { ChartLegend } from "./ChartLegend"; const margin = { top: 20, right: 20, bottom: 30, left: 50 }, width = 710 - margin.left - margin.right, @@ -210,52 +211,35 @@ export const MovingStackedAreaChart = ({ return (
-
-
-
-

Download - {storedData.totalDownloaded.toFixed(2)} Mbps

-
-
-
-

- {" "} - - HTTP - {storedData.httpDownloaded.toFixed(2)} Mbps -{" "} - {( - (storedData.httpDownloaded / storedData.totalDownloaded) * - 100 - ).toFixed(2)} - % -

-
-
-
-

- {" "} - - P2P - {storedData.p2pDownloaded.toFixed(2)} Mbps -{" "} - {storedData.p2pDownloaded - ? ( - (storedData.p2pDownloaded / storedData.totalDownloaded) * - 100 - ).toFixed(2) - : 0} - % -

-
-
-
-

Upload P2P - {storedData.p2pUploaded.toFixed(2)} Mbps

-
-
+ +
); }; + +const calculatePercentage = (part: number, total: number): string => { + if (total === 0) { + return "0"; // Or any other default/fallback string you prefer + } + return ((part / total) * 100).toFixed(2); +}; diff --git a/packages/p2p-media-loader-demo/src/components/chart.css b/packages/p2p-media-loader-demo/src/components/chart/chart.css similarity index 100% rename from packages/p2p-media-loader-demo/src/components/chart.css rename to packages/p2p-media-loader-demo/src/components/chart/chart.css From 6541a9820f62fcbdc3ef6152636414cc3ae1eff9 Mon Sep 17 00:00:00 2001 From: DimaDemchenko Date: Mon, 1 Apr 2024 17:50:41 +0300 Subject: [PATCH 12/21] implemented chart legends --- .../src/components/chart/StatsChart.tsx | 65 ++++++++++++------- .../src/components/chart/chart.css | 10 ++- 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/packages/p2p-media-loader-demo/src/components/chart/StatsChart.tsx b/packages/p2p-media-loader-demo/src/components/chart/StatsChart.tsx index ead630cc..c0d3680f 100644 --- a/packages/p2p-media-loader-demo/src/components/chart/StatsChart.tsx +++ b/packages/p2p-media-loader-demo/src/components/chart/StatsChart.tsx @@ -211,27 +211,48 @@ export const MovingStackedAreaChart = ({ return (
- - +
+ + +
); @@ -239,7 +260,7 @@ export const MovingStackedAreaChart = ({ const calculatePercentage = (part: number, total: number): string => { if (total === 0) { - return "0"; // Or any other default/fallback string you prefer + return 0; } return ((part / total) * 100).toFixed(2); }; diff --git a/packages/p2p-media-loader-demo/src/components/chart/chart.css b/packages/p2p-media-loader-demo/src/components/chart/chart.css index bc0140b7..69273729 100644 --- a/packages/p2p-media-loader-demo/src/components/chart/chart.css +++ b/packages/p2p-media-loader-demo/src/components/chart/chart.css @@ -1,5 +1,10 @@ -.chart-legend { +.legend-container { + margin-top: 10px; position: absolute; +} + +.chart-legend { + position: relative; margin-top: 30px; margin-left: 70px; height: 70px; @@ -7,7 +12,8 @@ font-size: 12px; color: #fff; background: #404040; - display: inline-block; + display: flex; + flex-direction: column; padding: 12px 5px; border-radius: 2px; opacity: 0.8; From 5e54f1dcd1ae2fb867c90dbfdd8e220572560d5f Mon Sep 17 00:00:00 2001 From: DimaDemchenko Date: Tue, 2 Apr 2024 10:30:47 +0300 Subject: [PATCH 13/21] refactored naming --- .../src/components/Demo.tsx | 20 +-- .../src/components/chart/StatsChart.tsx | 124 +++++++++--------- 2 files changed, 71 insertions(+), 73 deletions(-) diff --git a/packages/p2p-media-loader-demo/src/components/Demo.tsx b/packages/p2p-media-loader-demo/src/components/Demo.tsx index c3136943..a50eb840 100644 --- a/packages/p2p-media-loader-demo/src/components/Demo.tsx +++ b/packages/p2p-media-loader-demo/src/components/Demo.tsx @@ -17,19 +17,19 @@ declare global { const convertToMB = (bytes: number) => bytes / 1024 / 1024; -type DownloadStats = { - series1: number; - series2: number; - series3: number; +export type DownloadStats = { + httpDownloaded: number; + p2pDownloaded: number; + p2pUploaded: number; }; export type Player = (typeof PLAYERS)[number]; export const Demo = () => { const data = useRef({ - series1: 0, - series2: 0, - series3: 0, + httpDownloaded: 0, + p2pDownloaded: 0, + p2pUploaded: 0, }); const { queryParams, setURLQueryParams } = useQueryParams< "player" | "streamUrl" @@ -40,10 +40,10 @@ export const Demo = () => { (bytesLength: number, downloadSource: string) => { switch (downloadSource) { case "http": - data.current.series1 += convertToMB(bytesLength); + data.current.httpDownloaded += convertToMB(bytesLength); break; case "p2p": - data.current.series2 += convertToMB(bytesLength); + data.current.p2pDownloaded += convertToMB(bytesLength); break; default: break; @@ -53,7 +53,7 @@ export const Demo = () => { ); const onChunkUploaded = useCallback((bytesLength: number) => { - data.current.series3 += convertToMB(bytesLength); + data.current.p2pUploaded += convertToMB(bytesLength); }, []); const onPeerConnect = useCallback((peerId: string) => { diff --git a/packages/p2p-media-loader-demo/src/components/chart/StatsChart.tsx b/packages/p2p-media-loader-demo/src/components/chart/StatsChart.tsx index c0d3680f..e659c642 100644 --- a/packages/p2p-media-loader-demo/src/components/chart/StatsChart.tsx +++ b/packages/p2p-media-loader-demo/src/components/chart/StatsChart.tsx @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -// @ts-nocheck - d3 is not typed import React, { useEffect, useRef, useState } from "react"; import * as d3 from "d3"; @@ -17,19 +16,19 @@ type StatsChartProps = { }; type ChartsData = { - date: number; - series1: number; - series2: number; - series3: number; + seconds: number; + httpDownloaded: number; + p2pDownloaded: number; + p2pUploaded: number; }; const generateInitialStackedData = () => { const nowInSeconds = Math.floor(performance.now() / 1000); return Array.from({ length: 120 }, (_, i) => ({ - date: nowInSeconds - (120 - i), - series1: 0, - series2: 0, - series3: 0, + seconds: nowInSeconds - (120 - i), + httpDownloaded: 0, + p2pDownloaded: 0, + p2pUploaded: 0, })); }; @@ -56,30 +55,30 @@ export const MovingStackedAreaChart = ({ const intervalID = setInterval(() => { if (!downloadStatsRef.current) return; - const { series1, series2, series3 } = downloadStatsRef.current; - - setData((prevData) => { - const newData = { - date: Math.floor(performance.now() / 1000), - series1: series1, - series2: series2, - series3: series3 * -1, + const { httpDownloaded, p2pDownloaded, p2pUploaded } = + downloadStatsRef.current; + console.log("httpDownloaded", httpDownloaded); + setData((prevData: ChartsData[]) => { + const newData: ChartsData = { + seconds: Math.floor(performance.now() / 1000), + httpDownloaded: httpDownloaded, + p2pDownloaded: p2pDownloaded, + p2pUploaded: p2pUploaded * -1, }; return [...prevData.slice(1), newData]; }); setStoredData((prevData) => ({ - totalDownloaded: prevData.totalDownloaded + series1 + series2, - httpDownloaded: prevData.httpDownloaded + series1, - p2pDownloaded: prevData.p2pDownloaded + series2, - p2pUploaded: prevData.p2pUploaded + series3, + totalDownloaded: + prevData.totalDownloaded + httpDownloaded + p2pDownloaded, + httpDownloaded: prevData.httpDownloaded + httpDownloaded, + p2pDownloaded: prevData.p2pDownloaded + p2pDownloaded, + p2pUploaded: prevData.p2pUploaded + p2pUploaded, })); - downloadStatsRef.current = { - series1: 0, - series2: 0, - series3: 0, - }; + downloadStatsRef.current.httpDownloaded = 0; + downloadStatsRef.current.p2pDownloaded = 0; + downloadStatsRef.current.p2pUploaded = 0; }, 1000); return () => clearInterval(intervalID); @@ -96,55 +95,54 @@ export const MovingStackedAreaChart = ({ .append("g") .attr("transform", `translate(${margin.left},${margin.top})`); - const content = svg.select("g"); - - const series = d3.stack().keys(["series1", "series2"])(data); + const downloadStatsStack = d3 + .stack() + .keys(["httpDownloaded", "p2pDownloaded"])(data); - const maxStackedValue = d3.max(series, (serie) => - d3.max(serie, (d) => d[1]), + const maxDownloadValue = d3.max(downloadStatsStack, (stack) => + d3.max(stack, (d) => d[1]), ); - const minSeries3Value = d3.min(data, (d) => d.series3); + const maxP2PUploadValue = d3.min(data, (d) => d.p2pUploaded); const xScale = d3 .scaleLinear() - .domain(d3.extent(data, (d) => d.date)) + .domain(d3.extent(data, (d) => d.seconds)) .range([0, width]); const yScale = d3 .scaleLinear() - .domain([minSeries3Value, maxStackedValue]) + .domain([maxP2PUploadValue, maxDownloadValue]) .range([height, 0]); const color = d3 .scaleOrdinal() - .domain(["series1", "series2"]) + .domain(["httpDownloaded", "p2pDownloaded"]) .range([COLORS.yellow, COLORS.lightOrange]); - const area = d3 + const downloadStatsArea = d3 .area() - .x((d) => xScale(d.data.date)) + .x((d) => xScale(d.data.seconds)) .y0((d) => yScale(d[0])) .y1((d) => yScale(d[1])) .curve(d3.curveBasis); - - const areaSeries3 = d3 - .area() - .x((d) => xScale(d.date)) - .y0(() => yScale(0)) - .y1((d) => yScale(d.series3)) - .curve(d3.curveBasis); - - const line = d3 + const downloadStatAreaLine = d3 .line() - .x((d) => xScale(d.data.date)) + .x((d) => xScale(d.data.seconds)) .y((d) => yScale(d[1])) .curve(d3.curveBasis); - const lineSeries3 = d3 + const p2pUploadArea = d3 + .area() + .x((d) => xScale(d.seconds)) + .y0(() => yScale(0)) + .y1((d) => yScale(d.p2pUploaded)) + .curve(d3.curveBasis); + const p2pUploadLineArea = d3 .line() - .x((d) => xScale(d.date)) - .y((d) => yScale(d.series3)) + .x((d) => xScale(d.seconds)) + .y((d) => yScale(d.p2pUploaded)) .curve(d3.curveBasis); + const content = svg.select("g"); content.selectAll(".axis").remove(); // Axes content @@ -172,7 +170,7 @@ export const MovingStackedAreaChart = ({ .data([data]) .join("path") .attr("class", "series3-area") - .attr("d", areaSeries3) + .attr("d", p2pUploadArea) .style("fill", COLORS.lightBlue) .style("opacity", 0.7); content @@ -180,28 +178,28 @@ export const MovingStackedAreaChart = ({ .data([data]) .join("path") .attr("class", "series3-line") - .attr("d", lineSeries3) + .attr("d", p2pUploadLineArea) .style("fill", "none") .style("stroke", "blue") .style("stroke-width", 2); content .selectAll(".layer") - .data(series) + .data(downloadStatsStack) .enter() .append("path") .attr("class", "layer") - .attr("d", area) - .style("fill", (d, i) => color(i)) + .attr("d", downloadStatsArea) + .style("fill", (i) => color(i)) .style("opacity", 0.7); content .selectAll(".line") - .data(series) + .data(downloadStatsStack) .join("path") .attr("class", "line") - .attr("d", line) + .attr("d", downloadStatAreaLine) .style("fill", "none") - .style("stroke", (d, i) => color(i)) + .style("stroke", (i) => color(i)) .style("stroke-width", 1.5); return () => { @@ -236,19 +234,19 @@ export const MovingStackedAreaChart = ({ legendItems={[ { color: COLORS.torchRed, - content: `Download - ${data[data.length - 1].series1.toFixed(2)} Mbps`, + content: `Download - ${data[data.length - 1].httpDownloaded.toFixed(2)} Mbps`, }, { color: COLORS.yellow, - content: `- HTTP - ${data[data.length - 1].series1.toFixed(2)} Mbps`, + content: `- HTTP - ${data[data.length - 1].httpDownloaded.toFixed(2)} Mbps`, }, { color: COLORS.lightOrange, - content: `- P2P - ${data[data.length - 1].series2.toFixed(2)} Mbps`, + content: `- P2P - ${data[data.length - 1].p2pDownloaded.toFixed(2)} Mbps`, }, { color: COLORS.lightBlue, - content: `Upload P2P - ${data[data.length - 1].series3.toFixed(2)} Mbps`, + content: `Upload P2P - ${(data[data.length - 1].p2pUploaded * -1).toFixed(2)} Mbps`, }, ]} /> @@ -258,7 +256,7 @@ export const MovingStackedAreaChart = ({ ); }; -const calculatePercentage = (part: number, total: number): string => { +const calculatePercentage = (part: number, total: number) => { if (total === 0) { return 0; } From e3c9b98dfbd4c6aa4025adb357fee02fc6ec5ae6 Mon Sep 17 00:00:00 2001 From: DimaDemchenko Date: Tue, 2 Apr 2024 11:48:53 +0300 Subject: [PATCH 14/21] resolved all TS-type-errors --- .../src/components/chart/StatsChart.tsx | 101 +++++++++--------- 1 file changed, 52 insertions(+), 49 deletions(-) diff --git a/packages/p2p-media-loader-demo/src/components/chart/StatsChart.tsx b/packages/p2p-media-loader-demo/src/components/chart/StatsChart.tsx index e659c642..ab17728f 100644 --- a/packages/p2p-media-loader-demo/src/components/chart/StatsChart.tsx +++ b/packages/p2p-media-loader-demo/src/components/chart/StatsChart.tsx @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ - import React, { useEffect, useRef, useState } from "react"; import * as d3 from "d3"; import { DownloadStats } from "../Demo"; @@ -7,6 +5,23 @@ import { COLORS } from "../../constants"; import "./chart.css"; import { ChartLegend } from "./ChartLegend"; +const generateInitialStackedData = () => { + const nowInSeconds = Math.floor(performance.now() / 1000); + return Array.from({ length: 120 }, (_, i) => ({ + seconds: nowInSeconds - (120 - i), + httpDownloaded: 0, + p2pDownloaded: 0, + p2pUploaded: 0, + })); +}; + +const calculatePercentage = (part: number, total: number) => { + if (total === 0) { + return 0; + } + return ((part / total) * 100).toFixed(2); +}; + const margin = { top: 20, right: 20, bottom: 30, left: 50 }, width = 710 - margin.left - margin.right, height = 310 - margin.top - margin.bottom; @@ -17,27 +32,11 @@ type StatsChartProps = { type ChartsData = { seconds: number; - httpDownloaded: number; - p2pDownloaded: number; - p2pUploaded: number; -}; - -const generateInitialStackedData = () => { - const nowInSeconds = Math.floor(performance.now() / 1000); - return Array.from({ length: 120 }, (_, i) => ({ - seconds: nowInSeconds - (120 - i), - httpDownloaded: 0, - p2pDownloaded: 0, - p2pUploaded: 0, - })); -}; +} & DownloadStats; type StoredData = { totalDownloaded: number; - httpDownloaded: number; - p2pDownloaded: number; - p2pUploaded: number; -}; +} & DownloadStats; export const MovingStackedAreaChart = ({ downloadStatsRef, @@ -57,12 +56,12 @@ export const MovingStackedAreaChart = ({ const { httpDownloaded, p2pDownloaded, p2pUploaded } = downloadStatsRef.current; - console.log("httpDownloaded", httpDownloaded); + setData((prevData: ChartsData[]) => { const newData: ChartsData = { seconds: Math.floor(performance.now() / 1000), - httpDownloaded: httpDownloaded, - p2pDownloaded: p2pDownloaded, + httpDownloaded, + p2pDownloaded, p2pUploaded: p2pUploaded * -1, }; return [...prevData.slice(1), newData]; @@ -96,7 +95,7 @@ export const MovingStackedAreaChart = ({ .attr("transform", `translate(${margin.left},${margin.top})`); const downloadStatsStack = d3 - .stack() + .stack() .keys(["httpDownloaded", "p2pDownloaded"])(data); const maxDownloadValue = d3.max(downloadStatsStack, (stack) => @@ -104,13 +103,19 @@ export const MovingStackedAreaChart = ({ ); const maxP2PUploadValue = d3.min(data, (d) => d.p2pUploaded); + const defaultDomain = [0, 1]; + const extentDomain = d3.extent(data, (d) => d.seconds) as [number, number]; const xScale = d3 .scaleLinear() - .domain(d3.extent(data, (d) => d.seconds)) + .domain( + extentDomain.every((extent) => extent !== undefined) + ? extentDomain + : defaultDomain, + ) .range([0, width]); const yScale = d3 .scaleLinear() - .domain([maxP2PUploadValue, maxDownloadValue]) + .domain([maxP2PUploadValue ?? 0, maxDownloadValue ?? 1]) .range([height, 0]); const color = d3 @@ -119,25 +124,25 @@ export const MovingStackedAreaChart = ({ .range([COLORS.yellow, COLORS.lightOrange]); const downloadStatsArea = d3 - .area() + .area>() .x((d) => xScale(d.data.seconds)) .y0((d) => yScale(d[0])) .y1((d) => yScale(d[1])) .curve(d3.curveBasis); const downloadStatAreaLine = d3 - .line() + .line>() .x((d) => xScale(d.data.seconds)) .y((d) => yScale(d[1])) .curve(d3.curveBasis); const p2pUploadArea = d3 - .area() + .area() .x((d) => xScale(d.seconds)) .y0(() => yScale(0)) .y1((d) => yScale(d.p2pUploaded)) .curve(d3.curveBasis); const p2pUploadLineArea = d3 - .line() + .line() .x((d) => xScale(d.seconds)) .y((d) => yScale(d.p2pUploaded)) .curve(d3.curveBasis); @@ -149,7 +154,12 @@ export const MovingStackedAreaChart = ({ .append("g") .attr("class", "axis axis--x") .attr("transform", `translate(0,${height})`) - .call(d3.axisBottom(xScale).tickSize(-height).tickFormat("")) + .call( + d3 + .axisBottom(xScale) + .tickSize(-height) + .tickFormat(() => ""), + ) .selectAll("line") .attr("stroke", "#ddd") .attr("stroke-dasharray", "2,2"); @@ -164,20 +174,19 @@ export const MovingStackedAreaChart = ({ // Data layers content.selectAll(".layer").remove(); - content - .selectAll(".series3-area") + .selectAll(".p2p-upload-area") .data([data]) .join("path") - .attr("class", "series3-area") + .attr("class", "p2p-upload-area") .attr("d", p2pUploadArea) .style("fill", COLORS.lightBlue) .style("opacity", 0.7); content - .selectAll(".series3-line") + .selectAll(".p2p-upload-line") .data([data]) .join("path") - .attr("class", "series3-line") + .attr("class", "p2p-upload-line") .attr("d", p2pUploadLineArea) .style("fill", "none") .style("stroke", "blue") @@ -190,8 +199,9 @@ export const MovingStackedAreaChart = ({ .append("path") .attr("class", "layer") .attr("d", downloadStatsArea) - .style("fill", (i) => color(i)) + .style("fill", (_d, i) => color(i.toString()) as string) .style("opacity", 0.7); + content .selectAll(".line") .data(downloadStatsStack) @@ -199,7 +209,7 @@ export const MovingStackedAreaChart = ({ .attr("class", "line") .attr("d", downloadStatAreaLine) .style("fill", "none") - .style("stroke", (i) => color(i)) + .style("stroke", (_d, i) => color(i.toString()) as string) .style("stroke-width", 1.5); return () => { @@ -234,19 +244,19 @@ export const MovingStackedAreaChart = ({ legendItems={[ { color: COLORS.torchRed, - content: `Download - ${data[data.length - 1].httpDownloaded.toFixed(2)} Mbps`, + content: `Download - ${data[data.length - 1].httpDownloaded.toFixed(2)} Mb/s`, }, { color: COLORS.yellow, - content: `- HTTP - ${data[data.length - 1].httpDownloaded.toFixed(2)} Mbps`, + content: `- HTTP - ${data[data.length - 1].httpDownloaded.toFixed(2)} Mb/s`, }, { color: COLORS.lightOrange, - content: `- P2P - ${data[data.length - 1].p2pDownloaded.toFixed(2)} Mbps`, + content: `- P2P - ${data[data.length - 1].p2pDownloaded.toFixed(2)} Mb/s`, }, { color: COLORS.lightBlue, - content: `Upload P2P - ${(data[data.length - 1].p2pUploaded * -1).toFixed(2)} Mbps`, + content: `Upload P2P - ${(data[data.length - 1].p2pUploaded * -1).toFixed(2)} Mb/s`, }, ]} /> @@ -255,10 +265,3 @@ export const MovingStackedAreaChart = ({
); }; - -const calculatePercentage = (part: number, total: number) => { - if (total === 0) { - return 0; - } - return ((part / total) * 100).toFixed(2); -}; From 37f27d758ba1722a121496a73b88e87aeeaec07c Mon Sep 17 00:00:00 2001 From: DimaDemchenko Date: Tue, 2 Apr 2024 12:21:03 +0300 Subject: [PATCH 15/21] spacing for readability --- .../src/components/chart/StatsChart.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/p2p-media-loader-demo/src/components/chart/StatsChart.tsx b/packages/p2p-media-loader-demo/src/components/chart/StatsChart.tsx index ab17728f..09597d3b 100644 --- a/packages/p2p-media-loader-demo/src/components/chart/StatsChart.tsx +++ b/packages/p2p-media-loader-demo/src/components/chart/StatsChart.tsx @@ -105,6 +105,7 @@ export const MovingStackedAreaChart = ({ const defaultDomain = [0, 1]; const extentDomain = d3.extent(data, (d) => d.seconds) as [number, number]; + const xScale = d3 .scaleLinear() .domain( @@ -129,7 +130,7 @@ export const MovingStackedAreaChart = ({ .y0((d) => yScale(d[0])) .y1((d) => yScale(d[1])) .curve(d3.curveBasis); - const downloadStatAreaLine = d3 + const downloadStatsAreaLine = d3 .line>() .x((d) => xScale(d.data.seconds)) .y((d) => yScale(d[1])) @@ -149,6 +150,7 @@ export const MovingStackedAreaChart = ({ const content = svg.select("g"); content.selectAll(".axis").remove(); + // Axes content .append("g") @@ -163,7 +165,6 @@ export const MovingStackedAreaChart = ({ .selectAll("line") .attr("stroke", "#ddd") .attr("stroke-dasharray", "2,2"); - content .append("g") .attr("class", "axis axis--y") @@ -201,13 +202,12 @@ export const MovingStackedAreaChart = ({ .attr("d", downloadStatsArea) .style("fill", (_d, i) => color(i.toString()) as string) .style("opacity", 0.7); - content .selectAll(".line") .data(downloadStatsStack) .join("path") .attr("class", "line") - .attr("d", downloadStatAreaLine) + .attr("d", downloadStatsAreaLine) .style("fill", "none") .style("stroke", (_d, i) => color(i.toString()) as string) .style("stroke-width", 1.5); From 216532bfe6f90ce4a51e024a43efdd6c34c34a10 Mon Sep 17 00:00:00 2001 From: DimaDemchenko Date: Tue, 2 Apr 2024 12:29:03 +0300 Subject: [PATCH 16/21] renamed chart component --- packages/p2p-media-loader-demo/src/components/Demo.tsx | 7 +++---- .../chart/{StatsChart.tsx => DownloadStatsChart.tsx} | 4 +--- 2 files changed, 4 insertions(+), 7 deletions(-) rename packages/p2p-media-loader-demo/src/components/chart/{StatsChart.tsx => DownloadStatsChart.tsx} (98%) diff --git a/packages/p2p-media-loader-demo/src/components/Demo.tsx b/packages/p2p-media-loader-demo/src/components/Demo.tsx index a50eb840..c75d9591 100644 --- a/packages/p2p-media-loader-demo/src/components/Demo.tsx +++ b/packages/p2p-media-loader-demo/src/components/Demo.tsx @@ -4,10 +4,9 @@ import { PLAYERS } from "../constants"; import { useQueryParams } from "../hooks/useQueryParams"; import { HlsjsPlayer } from "./players/Hlsjs"; import { GraphNetwork } from "./GraphNetwork"; -import "./demo.css"; import { useCallback, useRef, useState } from "react"; -import { MovingStackedAreaChart } from "./chart/StatsChart"; - +import { DownloadStatsChart } from "./chart/DownloadStatsChart"; +import "./demo.css"; declare global { interface Window { Hls: typeof Hls; @@ -101,7 +100,7 @@ export const Demo = () => { />
- + ); }; diff --git a/packages/p2p-media-loader-demo/src/components/chart/StatsChart.tsx b/packages/p2p-media-loader-demo/src/components/chart/DownloadStatsChart.tsx similarity index 98% rename from packages/p2p-media-loader-demo/src/components/chart/StatsChart.tsx rename to packages/p2p-media-loader-demo/src/components/chart/DownloadStatsChart.tsx index 09597d3b..09eb7616 100644 --- a/packages/p2p-media-loader-demo/src/components/chart/StatsChart.tsx +++ b/packages/p2p-media-loader-demo/src/components/chart/DownloadStatsChart.tsx @@ -38,9 +38,7 @@ type StoredData = { totalDownloaded: number; } & DownloadStats; -export const MovingStackedAreaChart = ({ - downloadStatsRef, -}: StatsChartProps) => { +export const DownloadStatsChart = ({ downloadStatsRef }: StatsChartProps) => { const [data, setData] = useState(generateInitialStackedData()); const [storedData, setStoredData] = useState({ totalDownloaded: 0, From 5dd52c8ffde6c9f9719e69e017308a96645273a1 Mon Sep 17 00:00:00 2001 From: DimaDemchenko Date: Tue, 2 Apr 2024 12:36:52 +0300 Subject: [PATCH 17/21] fixed margin of svg container --- .../src/components/chart/DownloadStatsChart.tsx | 2 +- packages/p2p-media-loader-demo/src/components/chart/chart.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/p2p-media-loader-demo/src/components/chart/DownloadStatsChart.tsx b/packages/p2p-media-loader-demo/src/components/chart/DownloadStatsChart.tsx index 09eb7616..16c9dd65 100644 --- a/packages/p2p-media-loader-demo/src/components/chart/DownloadStatsChart.tsx +++ b/packages/p2p-media-loader-demo/src/components/chart/DownloadStatsChart.tsx @@ -22,7 +22,7 @@ const calculatePercentage = (part: number, total: number) => { return ((part / total) * 100).toFixed(2); }; -const margin = { top: 20, right: 20, bottom: 30, left: 50 }, +const margin = { top: 20, right: 20, bottom: 30, left: 20 }, width = 710 - margin.left - margin.right, height = 310 - margin.top - margin.bottom; diff --git a/packages/p2p-media-loader-demo/src/components/chart/chart.css b/packages/p2p-media-loader-demo/src/components/chart/chart.css index 69273729..bf1a371c 100644 --- a/packages/p2p-media-loader-demo/src/components/chart/chart.css +++ b/packages/p2p-media-loader-demo/src/components/chart/chart.css @@ -6,7 +6,7 @@ .chart-legend { position: relative; margin-top: 30px; - margin-left: 70px; + margin-left: 40px; height: 70px; font-family: Arial; font-size: 12px; From 5f77b1569e5ef7a2f9c8787e1bf9717068250815 Mon Sep 17 00:00:00 2001 From: DimaDemchenko Date: Tue, 2 Apr 2024 12:40:00 +0300 Subject: [PATCH 18/21] d3 types moved to dev deps --- packages/p2p-media-loader-demo/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/p2p-media-loader-demo/package.json b/packages/p2p-media-loader-demo/package.json index 09a5005c..18ee16b1 100644 --- a/packages/p2p-media-loader-demo/package.json +++ b/packages/p2p-media-loader-demo/package.json @@ -44,7 +44,6 @@ "clean-with-modules": "rimraf node_modules && pnpm clean" }, "dependencies": { - "@types/d3": "^7.4.3", "d3": "^7.9.0", "hls.js": "^1.5.7", "p2p-media-loader-core": "workspace:*", @@ -53,6 +52,7 @@ "vis-network": "^9.1.9" }, "devDependencies": { + "@types/d3": "^7.4.3", "@types/dplayer": "^1.25.5", "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", From 1ea67fa6f641098032fe207a21739758056cb591 Mon Sep 17 00:00:00 2001 From: DimaDemchenko Date: Tue, 2 Apr 2024 16:29:49 +0300 Subject: [PATCH 19/21] improvements --- .../src/components/Demo.tsx | 10 +- .../components/chart/DownloadStatsChart.tsx | 166 +++--------------- .../src/components/chart/chart.css | 6 + .../src/components/chart/drawChart.ts | 140 +++++++++++++++ 4 files changed, 172 insertions(+), 150 deletions(-) create mode 100644 packages/p2p-media-loader-demo/src/components/chart/drawChart.ts diff --git a/packages/p2p-media-loader-demo/src/components/Demo.tsx b/packages/p2p-media-loader-demo/src/components/Demo.tsx index c75d9591..f8e1b9b0 100644 --- a/packages/p2p-media-loader-demo/src/components/Demo.tsx +++ b/packages/p2p-media-loader-demo/src/components/Demo.tsx @@ -14,8 +14,6 @@ declare global { } } -const convertToMB = (bytes: number) => bytes / 1024 / 1024; - export type DownloadStats = { httpDownloaded: number; p2pDownloaded: number; @@ -30,19 +28,21 @@ export const Demo = () => { p2pDownloaded: 0, p2pUploaded: 0, }); + const { queryParams, setURLQueryParams } = useQueryParams< "player" | "streamUrl" >(); + const [peers, setPeers] = useState([]); const onChunkDownloaded = useCallback( (bytesLength: number, downloadSource: string) => { switch (downloadSource) { case "http": - data.current.httpDownloaded += convertToMB(bytesLength); + data.current.httpDownloaded += bytesLength; break; case "p2p": - data.current.p2pDownloaded += convertToMB(bytesLength); + data.current.p2pDownloaded += bytesLength; break; default: break; @@ -52,7 +52,7 @@ export const Demo = () => { ); const onChunkUploaded = useCallback((bytesLength: number) => { - data.current.p2pUploaded += convertToMB(bytesLength); + data.current.p2pUploaded += bytesLength; }, []); const onPeerConnect = useCallback((peerId: string) => { diff --git a/packages/p2p-media-loader-demo/src/components/chart/DownloadStatsChart.tsx b/packages/p2p-media-loader-demo/src/components/chart/DownloadStatsChart.tsx index 16c9dd65..af898ae0 100644 --- a/packages/p2p-media-loader-demo/src/components/chart/DownloadStatsChart.tsx +++ b/packages/p2p-media-loader-demo/src/components/chart/DownloadStatsChart.tsx @@ -1,9 +1,9 @@ import React, { useEffect, useRef, useState } from "react"; -import * as d3 from "d3"; import { DownloadStats } from "../Demo"; import { COLORS } from "../../constants"; import "./chart.css"; import { ChartLegend } from "./ChartLegend"; +import { drawChart } from "./drawChart"; const generateInitialStackedData = () => { const nowInSeconds = Math.floor(performance.now() / 1000); @@ -15,6 +15,9 @@ const generateInitialStackedData = () => { })); }; +const convertToMiB = (bytes: number) => bytes / 1024 / 1024; +const convertToMbit = (bytes: number) => (bytes * 8) / 1_000_000; + const calculatePercentage = (part: number, total: number) => { if (total === 0) { return 0; @@ -22,15 +25,11 @@ const calculatePercentage = (part: number, total: number) => { return ((part / total) * 100).toFixed(2); }; -const margin = { top: 20, right: 20, bottom: 30, left: 20 }, - width = 710 - margin.left - margin.right, - height = 310 - margin.top - margin.bottom; - type StatsChartProps = { downloadStatsRef: React.RefObject; }; -type ChartsData = { +export type ChartsData = { seconds: number; } & DownloadStats; @@ -58,19 +57,20 @@ export const DownloadStatsChart = ({ downloadStatsRef }: StatsChartProps) => { setData((prevData: ChartsData[]) => { const newData: ChartsData = { seconds: Math.floor(performance.now() / 1000), - httpDownloaded, - p2pDownloaded, - p2pUploaded: p2pUploaded * -1, + httpDownloaded: convertToMbit(httpDownloaded), + p2pDownloaded: convertToMbit(p2pDownloaded), + p2pUploaded: convertToMbit(p2pUploaded * -1), }; return [...prevData.slice(1), newData]; }); setStoredData((prevData) => ({ totalDownloaded: - prevData.totalDownloaded + httpDownloaded + p2pDownloaded, - httpDownloaded: prevData.httpDownloaded + httpDownloaded, - p2pDownloaded: prevData.p2pDownloaded + p2pDownloaded, - p2pUploaded: prevData.p2pUploaded + p2pUploaded, + prevData.totalDownloaded + + convertToMiB(httpDownloaded + p2pDownloaded), + httpDownloaded: prevData.httpDownloaded + convertToMiB(httpDownloaded), + p2pDownloaded: prevData.p2pDownloaded + convertToMiB(p2pDownloaded), + p2pUploaded: prevData.p2pUploaded + convertToMiB(p2pUploaded), })); downloadStatsRef.current.httpDownloaded = 0; @@ -84,131 +84,7 @@ export const DownloadStatsChart = ({ downloadStatsRef }: StatsChartProps) => { useEffect(() => { if (!svgRef.current) return; - const svg = d3.select(svgRef.current); - svg - .selectAll("g") - .data([null]) - .enter() - .append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - const downloadStatsStack = d3 - .stack() - .keys(["httpDownloaded", "p2pDownloaded"])(data); - - const maxDownloadValue = d3.max(downloadStatsStack, (stack) => - d3.max(stack, (d) => d[1]), - ); - const maxP2PUploadValue = d3.min(data, (d) => d.p2pUploaded); - - const defaultDomain = [0, 1]; - const extentDomain = d3.extent(data, (d) => d.seconds) as [number, number]; - - const xScale = d3 - .scaleLinear() - .domain( - extentDomain.every((extent) => extent !== undefined) - ? extentDomain - : defaultDomain, - ) - .range([0, width]); - const yScale = d3 - .scaleLinear() - .domain([maxP2PUploadValue ?? 0, maxDownloadValue ?? 1]) - .range([height, 0]); - - const color = d3 - .scaleOrdinal() - .domain(["httpDownloaded", "p2pDownloaded"]) - .range([COLORS.yellow, COLORS.lightOrange]); - - const downloadStatsArea = d3 - .area>() - .x((d) => xScale(d.data.seconds)) - .y0((d) => yScale(d[0])) - .y1((d) => yScale(d[1])) - .curve(d3.curveBasis); - const downloadStatsAreaLine = d3 - .line>() - .x((d) => xScale(d.data.seconds)) - .y((d) => yScale(d[1])) - .curve(d3.curveBasis); - - const p2pUploadArea = d3 - .area() - .x((d) => xScale(d.seconds)) - .y0(() => yScale(0)) - .y1((d) => yScale(d.p2pUploaded)) - .curve(d3.curveBasis); - const p2pUploadLineArea = d3 - .line() - .x((d) => xScale(d.seconds)) - .y((d) => yScale(d.p2pUploaded)) - .curve(d3.curveBasis); - - const content = svg.select("g"); - content.selectAll(".axis").remove(); - - // Axes - content - .append("g") - .attr("class", "axis axis--x") - .attr("transform", `translate(0,${height})`) - .call( - d3 - .axisBottom(xScale) - .tickSize(-height) - .tickFormat(() => ""), - ) - .selectAll("line") - .attr("stroke", "#ddd") - .attr("stroke-dasharray", "2,2"); - content - .append("g") - .attr("class", "axis axis--y") - .call(d3.axisLeft(yScale).tickSize(-width).ticks(5)) - .selectAll("line") - .attr("stroke", "#ddd") - .attr("stroke-dasharray", "2,2"); - - // Data layers - content.selectAll(".layer").remove(); - content - .selectAll(".p2p-upload-area") - .data([data]) - .join("path") - .attr("class", "p2p-upload-area") - .attr("d", p2pUploadArea) - .style("fill", COLORS.lightBlue) - .style("opacity", 0.7); - content - .selectAll(".p2p-upload-line") - .data([data]) - .join("path") - .attr("class", "p2p-upload-line") - .attr("d", p2pUploadLineArea) - .style("fill", "none") - .style("stroke", "blue") - .style("stroke-width", 2); - - content - .selectAll(".layer") - .data(downloadStatsStack) - .enter() - .append("path") - .attr("class", "layer") - .attr("d", downloadStatsArea) - .style("fill", (_d, i) => color(i.toString()) as string) - .style("opacity", 0.7); - content - .selectAll(".line") - .data(downloadStatsStack) - .join("path") - .attr("class", "line") - .attr("d", downloadStatsAreaLine) - .style("fill", "none") - .style("stroke", (_d, i) => color(i.toString()) as string) - .style("stroke-width", 1.5); + const svg = drawChart(svgRef, data); return () => { svg.selectAll("*").remove(); @@ -222,19 +98,19 @@ export const DownloadStatsChart = ({ downloadStatsRef }: StatsChartProps) => { legendItems={[ { color: COLORS.torchRed, - content: `Download - ${storedData.totalDownloaded.toFixed(2)} Mb `, + content: `Download - ${storedData.totalDownloaded.toFixed(2)} MiB `, }, { color: COLORS.yellow, - content: `- HTTP - ${storedData.httpDownloaded.toFixed(2)} Mb - ${calculatePercentage(storedData.httpDownloaded, storedData.totalDownloaded)}%`, + content: `- HTTP - ${storedData.httpDownloaded.toFixed(2)} MiB - ${calculatePercentage(storedData.httpDownloaded, storedData.totalDownloaded)}%`, }, { color: COLORS.lightOrange, - content: `- P2P - ${storedData.p2pDownloaded.toFixed(2)} Mb - ${calculatePercentage(storedData.p2pDownloaded, storedData.totalDownloaded)}%`, + content: `- P2P - ${storedData.p2pDownloaded.toFixed(2)} MiB - ${calculatePercentage(storedData.p2pDownloaded, storedData.totalDownloaded)}%`, }, { color: COLORS.lightBlue, - content: `Upload P2P - ${storedData.p2pUploaded.toFixed(2)} Mb`, + content: `Upload P2P - ${storedData.p2pUploaded.toFixed(2)} MiB`, }, ]} /> @@ -242,15 +118,15 @@ export const DownloadStatsChart = ({ downloadStatsRef }: StatsChartProps) => { legendItems={[ { color: COLORS.torchRed, - content: `Download - ${data[data.length - 1].httpDownloaded.toFixed(2)} Mb/s`, + content: `Download - ${data[data.length - 1].httpDownloaded.toFixed(2)} Mbit/s`, }, { color: COLORS.yellow, - content: `- HTTP - ${data[data.length - 1].httpDownloaded.toFixed(2)} Mb/s`, + content: `- HTTP - ${data[data.length - 1].httpDownloaded.toFixed(2)} Mbit/s`, }, { color: COLORS.lightOrange, - content: `- P2P - ${data[data.length - 1].p2pDownloaded.toFixed(2)} Mb/s`, + content: `- P2P - ${data[data.length - 1].p2pDownloaded.toFixed(2)} Mbit/s`, }, { color: COLORS.lightBlue, diff --git a/packages/p2p-media-loader-demo/src/components/chart/chart.css b/packages/p2p-media-loader-demo/src/components/chart/chart.css index bf1a371c..bee743ba 100644 --- a/packages/p2p-media-loader-demo/src/components/chart/chart.css +++ b/packages/p2p-media-loader-demo/src/components/chart/chart.css @@ -24,6 +24,12 @@ color: #fff; } +.chart-legend p::before { + content: ""; + display: inline-block; + width: 4px; +} + .chart-legend .line { margin-right: 3px; margin-left: 4px; diff --git a/packages/p2p-media-loader-demo/src/components/chart/drawChart.ts b/packages/p2p-media-loader-demo/src/components/chart/drawChart.ts new file mode 100644 index 00000000..860db527 --- /dev/null +++ b/packages/p2p-media-loader-demo/src/components/chart/drawChart.ts @@ -0,0 +1,140 @@ +import { COLORS } from "../../constants"; +import { ChartsData } from "./DownloadStatsChart"; +import * as d3 from "d3"; + +const margin = { top: 20, right: 20, bottom: 30, left: 25 }, + width = 710 - margin.left - margin.right, + height = 310 - margin.top - margin.bottom; + +export const drawChart = ( + svgRef: React.RefObject, + data: ChartsData[], +) => { + const svg = d3.select(svgRef.current); + svg + .selectAll("g") + .data([null]) + .enter() + .append("g") + .attr("transform", `translate(${margin.left},${margin.top})`); + + const downloadStatsStack = d3 + .stack() + .keys(["httpDownloaded", "p2pDownloaded"])(data); + + const maxDownloadValue = d3.max(downloadStatsStack, (stack) => + d3.max(stack, (d) => d[1]), + ); + const maxP2PUploadValue = d3.min(data, (d) => d.p2pUploaded); + + const defaultDomain = [0, 1]; + const extentDomain = d3.extent(data, (d) => d.seconds) as [number, number]; + + const xScale = d3 + .scaleLinear() + .domain( + extentDomain.every((extent) => extent !== undefined) + ? extentDomain + : defaultDomain, + ) + .range([0, width]); + const yScale = d3 + .scaleLinear() + .domain([maxP2PUploadValue ?? 0, maxDownloadValue ?? 1]) + .range([height, 0]); + + const color = d3 + .scaleOrdinal() + .domain(["httpDownloaded", "p2pDownloaded"]) + .range([COLORS.yellow, COLORS.lightOrange]); + + const downloadStatsArea = d3 + .area>() + .x((d) => xScale(d.data.seconds)) + .y0((d) => yScale(d[0])) + .y1((d) => yScale(d[1])) + .curve(d3.curveBasis); + const downloadStatsAreaLine = d3 + .line>() + .x((d) => xScale(d.data.seconds)) + .y((d) => yScale(d[1])) + .curve(d3.curveBasis); + + const p2pUploadArea = d3 + .area() + .x((d) => xScale(d.seconds)) + .y0(() => yScale(0)) + .y1((d) => yScale(d.p2pUploaded)) + .curve(d3.curveBasis); + const p2pUploadLineArea = d3 + .line() + .x((d) => xScale(d.seconds)) + .y((d) => yScale(d.p2pUploaded)) + .curve(d3.curveBasis); + + const content = svg.select("g"); + content.selectAll(".axis").remove(); + + // Axes + content + .append("g") + .attr("class", "axis axis--x") + .attr("transform", `translate(0,${height})`) + .call( + d3 + .axisBottom(xScale) + .tickSize(-height) + .tickFormat(() => ""), + ) + .selectAll("line") + .attr("stroke", "#ddd") + .attr("stroke-dasharray", "2,2"); + content + .append("g") + .attr("class", "axis axis--y") + .call(d3.axisLeft(yScale).tickSize(-width).ticks(5)) + .selectAll("line") + .attr("stroke", "#ddd") + .attr("stroke-dasharray", "2,2"); + + // Data layers + content.selectAll(".layer").remove(); + content + .selectAll(".p2p-upload-area") + .data([data]) + .join("path") + .attr("class", "p2p-upload-area") + .attr("d", p2pUploadArea) + .style("fill", COLORS.lightBlue) + .style("opacity", 0.7); + content + .selectAll(".p2p-upload-line") + .data([data]) + .join("path") + .attr("class", "p2p-upload-line") + .attr("d", p2pUploadLineArea) + .style("fill", "none") + .style("stroke", "blue") + .style("stroke-width", 2); + + content + .selectAll(".layer") + .data(downloadStatsStack) + .enter() + .append("path") + .attr("class", "layer") + .attr("d", downloadStatsArea) + .style("fill", (_d, i) => color(i.toString()) as string) + .style("opacity", 0.7); + content + .selectAll(".line") + .data(downloadStatsStack) + .join("path") + .attr("class", "line") + .attr("d", downloadStatsAreaLine) + .style("fill", "none") + .style("stroke", (_d, i) => color(i.toString()) as string) + .style("stroke-width", 1.5); + + return svg; +}; From ffed8df32e566276bd68335f8ae60972c0d9c432 Mon Sep 17 00:00:00 2001 From: DimaDemchenko Date: Tue, 2 Apr 2024 16:35:32 +0300 Subject: [PATCH 20/21] mbit/s --- .../src/components/chart/DownloadStatsChart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/p2p-media-loader-demo/src/components/chart/DownloadStatsChart.tsx b/packages/p2p-media-loader-demo/src/components/chart/DownloadStatsChart.tsx index af898ae0..0b7f8299 100644 --- a/packages/p2p-media-loader-demo/src/components/chart/DownloadStatsChart.tsx +++ b/packages/p2p-media-loader-demo/src/components/chart/DownloadStatsChart.tsx @@ -130,7 +130,7 @@ export const DownloadStatsChart = ({ downloadStatsRef }: StatsChartProps) => { }, { color: COLORS.lightBlue, - content: `Upload P2P - ${(data[data.length - 1].p2pUploaded * -1).toFixed(2)} Mb/s`, + content: `Upload P2P - ${(data[data.length - 1].p2pUploaded * -1).toFixed(2)} Mbit/s`, }, ]} /> From d40df0b08201c2dca1c2abce08dbfa2724074621 Mon Sep 17 00:00:00 2001 From: DimaDemchenko Date: Wed, 3 Apr 2024 09:00:14 +0300 Subject: [PATCH 21/21] removed unused deps --- packages/p2p-media-loader-demo/package.json | 1 - pnpm-lock.yaml | 84 +++++++++------------ 2 files changed, 35 insertions(+), 50 deletions(-) diff --git a/packages/p2p-media-loader-demo/package.json b/packages/p2p-media-loader-demo/package.json index 18ee16b1..3a5f4966 100644 --- a/packages/p2p-media-loader-demo/package.json +++ b/packages/p2p-media-loader-demo/package.json @@ -48,7 +48,6 @@ "hls.js": "^1.5.7", "p2p-media-loader-core": "workspace:*", "p2p-media-loader-hlsjs": "workspace:*", - "rickshaw": "^1.7.1", "vis-network": "^9.1.9" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9fe5330f..9816f1f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -106,9 +106,6 @@ importers: packages/p2p-media-loader-demo: dependencies: - '@types/d3': - specifier: ^7.4.3 - version: 7.4.3 d3: specifier: ^7.9.0 version: 7.9.0 @@ -121,13 +118,13 @@ importers: p2p-media-loader-hlsjs: specifier: workspace:* version: link:../p2p-media-loader-hlsjs - rickshaw: - specifier: ^1.7.1 - version: 1.7.1 vis-network: specifier: ^9.1.9 version: 9.1.9(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.4.0)(uuid@9.0.1)(vis-data@7.1.9)(vis-util@5.0.7) devDependencies: + '@types/d3': + specifier: ^7.4.3 + version: 7.4.3 '@types/dplayer': specifier: ^1.25.5 version: 1.25.5 @@ -946,147 +943,147 @@ packages: /@types/d3-array@3.2.1: resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} - dev: false + dev: true /@types/d3-axis@3.0.6: resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} dependencies: '@types/d3-selection': 3.0.10 - dev: false + dev: true /@types/d3-brush@3.0.6: resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} dependencies: '@types/d3-selection': 3.0.10 - dev: false + dev: true /@types/d3-chord@3.0.6: resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} - dev: false + dev: true /@types/d3-color@3.1.3: resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} - dev: false + dev: true /@types/d3-contour@3.0.6: resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} dependencies: '@types/d3-array': 3.2.1 '@types/geojson': 7946.0.14 - dev: false + dev: true /@types/d3-delaunay@6.0.4: resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} - dev: false + dev: true /@types/d3-dispatch@3.0.6: resolution: {integrity: sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==} - dev: false + dev: true /@types/d3-drag@3.0.7: resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} dependencies: '@types/d3-selection': 3.0.10 - dev: false + dev: true /@types/d3-dsv@3.0.7: resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} - dev: false + dev: true /@types/d3-ease@3.0.2: resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} - dev: false + dev: true /@types/d3-fetch@3.0.7: resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} dependencies: '@types/d3-dsv': 3.0.7 - dev: false + dev: true /@types/d3-force@3.0.9: resolution: {integrity: sha512-IKtvyFdb4Q0LWna6ymywQsEYjK/94SGhPrMfEr1TIc5OBeziTi+1jcCvttts8e0UWZIxpasjnQk9MNk/3iS+kA==} - dev: false + dev: true /@types/d3-format@3.0.4: resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} - dev: false + dev: true /@types/d3-geo@3.1.0: resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} dependencies: '@types/geojson': 7946.0.14 - dev: false + dev: true /@types/d3-hierarchy@3.1.7: resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} - dev: false + dev: true /@types/d3-interpolate@3.0.4: resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} dependencies: '@types/d3-color': 3.1.3 - dev: false + dev: true /@types/d3-path@3.1.0: resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==} - dev: false + dev: true /@types/d3-polygon@3.0.2: resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} - dev: false + dev: true /@types/d3-quadtree@3.0.6: resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} - dev: false + dev: true /@types/d3-random@3.0.3: resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} - dev: false + dev: true /@types/d3-scale-chromatic@3.0.3: resolution: {integrity: sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==} - dev: false + dev: true /@types/d3-scale@4.0.8: resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==} dependencies: '@types/d3-time': 3.0.3 - dev: false + dev: true /@types/d3-selection@3.0.10: resolution: {integrity: sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==} - dev: false + dev: true /@types/d3-shape@3.1.6: resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==} dependencies: '@types/d3-path': 3.1.0 - dev: false + dev: true /@types/d3-time-format@4.0.3: resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} - dev: false + dev: true /@types/d3-time@3.0.3: resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==} - dev: false + dev: true /@types/d3-timer@3.0.2: resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} - dev: false + dev: true /@types/d3-transition@3.0.8: resolution: {integrity: sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==} dependencies: '@types/d3-selection': 3.0.10 - dev: false + dev: true /@types/d3-zoom@3.0.8: resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} dependencies: '@types/d3-interpolate': 3.0.4 '@types/d3-selection': 3.0.10 - dev: false + dev: true /@types/d3@7.4.3: resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} @@ -1121,7 +1118,7 @@ packages: '@types/d3-timer': 3.0.2 '@types/d3-transition': 3.0.8 '@types/d3-zoom': 3.0.8 - dev: false + dev: true /@types/debug@4.1.12: resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -1139,7 +1136,7 @@ packages: /@types/geojson@7946.0.14: resolution: {integrity: sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==} - dev: false + dev: true /@types/hammerjs@2.0.45: resolution: {integrity: sha512-qkcUlZmX6c4J8q45taBKTL3p+LbITgyx7qhlPYOdOHZB7B31K0mXbP5YA7i7SgDeEGuI9MnumiKPEMrxg8j3KQ==} @@ -2122,10 +2119,6 @@ packages: d3-transition: 3.0.1(d3-selection@3.0.0) dev: false - /d3@3.5.17: - resolution: {integrity: sha512-yFk/2idb8OHPKkbAL8QaOaqENNoMhIaSHZerk3oQsECwkObkCpJyjYwCe+OHiq6UEdhe1m8ZGARRRO3ljFjlKg==} - dev: false - /d3@7.9.0: resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} engines: {node: '>=12'} @@ -3998,13 +3991,6 @@ packages: engines: {iojs: '>=1.0.0', node: '>=0.10.0'} dev: true - /rickshaw@1.7.1: - resolution: {integrity: sha512-H1eNOST/N8yIJPqdrZjW4R2r4hDN8zbtbBnvm2TzIYZhnfqmQr/fG/f8LMcvDMx1UrIzuBOPH2FTT9t0w79JWA==} - engines: {node: '>= 4.0.0'} - dependencies: - d3: 3.5.17 - dev: false - /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true