From c12eb186c5268da299f7c5f74aac9e25760b3195 Mon Sep 17 00:00:00 2001 From: MaikNeubert Date: Thu, 2 Nov 2023 18:38:17 +0100 Subject: [PATCH 001/178] first version of download --- .../custom-graphs/BarChartCenterAxis.tsx | 2 + .../src/components/nivo-graphs/BarChart.tsx | 6 +- statviz/src/state/selectedCharts.tsx | 19 ++++++ .../Statistics/components/BoxFlowSankey.tsx | 11 +--- .../Statistics/components/CreatedBoxes.tsx | 26 +++----- .../components/DemographicChart.tsx | 13 ++-- .../Statistics/components/TopProducts.tsx | 13 ++-- .../views/Statistics/components/VisHeader.tsx | 59 ++++++++++++++----- 8 files changed, 90 insertions(+), 59 deletions(-) create mode 100644 statviz/src/state/selectedCharts.tsx diff --git a/statviz/src/components/custom-graphs/BarChartCenterAxis.tsx b/statviz/src/components/custom-graphs/BarChartCenterAxis.tsx index c6403965d..f134b53f6 100644 --- a/statviz/src/components/custom-graphs/BarChartCenterAxis.tsx +++ b/statviz/src/components/custom-graphs/BarChartCenterAxis.tsx @@ -23,6 +23,7 @@ export interface IBarChartCenterAxis { background: string; colorBarLeft: string; colorBarRight: string; + visId: string; settings?: { hideZeroY?: boolean; hideZeroX?: boolean; @@ -116,6 +117,7 @@ export default function BarChartCenterAxis(chart: IBarChartCenterAxis) { ; + visId: string; keys?: Array; indexBy?: string; ariaLabel?: string; @@ -43,7 +44,10 @@ export default function BarChart(barChart: BarChart) { : []; return ( -
+
([]); + +export const select = (charts: string[], id: string) => { + if (charts.indexOf(id) !== -1) { + charts.push(id); + selectedCharts(charts); + } +}; + +export const deselect = (charts: string[], id: string) => { + if (charts.indexOf(id) !== -1) { + selectedCharts(charts.slice(charts.indexOf(id), 1)); + } +}; + +export const isSelected = (charts: string[], id: string) => + charts.indexOf(id) !== -1; diff --git a/statviz/src/views/Statistics/components/BoxFlowSankey.tsx b/statviz/src/views/Statistics/components/BoxFlowSankey.tsx index 2a3c1e291..afa39c5b0 100644 --- a/statviz/src/views/Statistics/components/BoxFlowSankey.tsx +++ b/statviz/src/views/Statistics/components/BoxFlowSankey.tsx @@ -4,16 +4,9 @@ import { Card, CardBody } from "@chakra-ui/react"; import { getSelectionBackground } from "../../../utils/theme"; export default function BoxFlowSankey() { - const [selected, setSelected] = useState(false); - return ( - - setSelected(true)} - onDeselect={() => setSelected(false)} - /> + + WIP Sankey ); diff --git a/statviz/src/views/Statistics/components/CreatedBoxes.tsx b/statviz/src/views/Statistics/components/CreatedBoxes.tsx index 5fb5e5792..fd55ae787 100644 --- a/statviz/src/views/Statistics/components/CreatedBoxes.tsx +++ b/statviz/src/views/Statistics/components/CreatedBoxes.tsx @@ -1,13 +1,5 @@ import { ApolloError, useQuery, gql } from "@apollo/client"; -import { - Card, - CardBody, - CardHeader, - Checkbox, - Flex, - Heading, - Spacer, -} from "@chakra-ui/react"; +import { Card, CardBody } from "@chakra-ui/react"; import _ from "lodash"; import { @@ -15,12 +7,11 @@ import { QueryCreatedBoxesArgs, } from "../../../types/generated/graphql"; import BarChart from "../../../components/nivo-graphs/BarChart"; -import { useMemo, useState } from "react"; +import { useMemo } from "react"; import useCreatedBoxes from "../../../utils/hooks/useCreatedBoxes"; import { useParams } from "react-router-dom"; import { BoxesOrItemsCount } from "../../Dashboard/Dashboard"; import VisHeader from "./VisHeader"; -import { getSelectionBackground } from "../../../utils/theme"; import NoDataCard from "./NoDataCard"; const CREATED_BOXES_QUERY = gql` @@ -48,6 +39,8 @@ const CREATED_BOXES_QUERY = gql` } `; +const visId = "created-boxes"; + export default function CreatedBoxes(params: { width: string; height: string; @@ -59,7 +52,6 @@ export default function CreatedBoxes(params: { QueryCreatedBoxesArgs >(CREATED_BOXES_QUERY, { variables: { baseId: parseInt(baseId) } }); const createdBoxes = useCreatedBoxes(data); - const [selected, setSelected] = useState(false); const getChartData = () => { if (data === undefined) return []; @@ -89,15 +81,11 @@ export default function CreatedBoxes(params: { } return ( - - setSelected(true)} - onDeselect={() => setSelected(false)} - > + + (false); const facts = [...props.cube.facts]; if (facts.length === 0) { @@ -89,19 +90,15 @@ export default function DemographicChart(props: { background: "#ffffff", colorBarLeft: "#ec5063", colorBarRight: "#31cab5", + visId: visId, settings: { hideZeroY: false, }, }; return ( - - setSelected(true)} - onDeselect={() => setSelected(false)} - > + + diff --git a/statviz/src/views/Statistics/components/TopProducts.tsx b/statviz/src/views/Statistics/components/TopProducts.tsx index f4b25c1bd..641f9dade 100644 --- a/statviz/src/views/Statistics/components/TopProducts.tsx +++ b/statviz/src/views/Statistics/components/TopProducts.tsx @@ -42,6 +42,8 @@ const CREATED_BOXES_QUERY = gql` } `; +const visId = "top-products"; + export default function TopProducts(params: { width: string; height: string; @@ -53,7 +55,6 @@ export default function TopProducts(params: { CreatedBoxesData, QueryCreatedBoxesArgs >(CREATED_BOXES_QUERY, { variables: { baseId: parseInt(baseId) } }); - const [selected, setSelected] = useState(false); const createdBoxes = useCreatedBoxes(data); const getChartData = () => { @@ -101,15 +102,11 @@ export default function TopProducts(params: { return ; } return ( - - setSelected(true)} - onDeselect={() => setSelected(false)} - > + + void; - onDeselect: () => void; + custom?: boolean; }) { - const [checked, setChecked] = useState(false); + const download = () => { + const chart = params.custom + ? document.getElementById(params.visId) + : document.getElementById(params.visId)?.firstChild?.firstChild + ?.firstChild; - const select = (event) => { - if (event.target.checked) { - setChecked(true); - params.onSelect(); - } else { - setChecked(false); - params.onDeselect(); + const serializer = new XMLSerializer(); + let source = serializer.serializeToString(chart); + + //add name spaces. + if (!source.match(/^]+xmlns="http\:\/\/www\.w3\.org\/2000\/svg"/)) { + source = source.replace( + /^]+"http\:\/\/www\.w3\.org\/1999\/xlink"/)) { + source = source.replace( + /^ {params.heading} - + ); From 72dd3070c4a91723a85fac039b44ab81766afb5c Mon Sep 17 00:00:00 2001 From: MaikNeubert Date: Fri, 3 Nov 2023 10:49:15 +0100 Subject: [PATCH 002/178] expand vis header with download control form --- statviz/package.json | 3 +- .../Statistics/components/CreatedBoxes.tsx | 6 +- .../components/DemographicChart.tsx | 7 +- .../Statistics/components/TopProducts.tsx | 6 +- .../views/Statistics/components/VisHeader.tsx | 194 ++++++++++++++---- statviz/yarn.lock | 5 + 6 files changed, 178 insertions(+), 43 deletions(-) diff --git a/statviz/package.json b/statviz/package.json index c251e59a6..447082a03 100644 --- a/statviz/package.json +++ b/statviz/package.json @@ -38,7 +38,8 @@ "react-dom": "^18.2.0", "react-hook-form": "^7.43.1", "react-router-dom": "^6.15.0", - "zod": "^3.22.3" + "zod": "^3.22.3", + "dom-to-image": "^2.6.0" }, "devDependencies": { "@graphql-codegen/cli": "5.0.0", diff --git a/statviz/src/views/Statistics/components/CreatedBoxes.tsx b/statviz/src/views/Statistics/components/CreatedBoxes.tsx index fd55ae787..de0bb3d84 100644 --- a/statviz/src/views/Statistics/components/CreatedBoxes.tsx +++ b/statviz/src/views/Statistics/components/CreatedBoxes.tsx @@ -82,7 +82,11 @@ export default function CreatedBoxes(params: { return ( - + - + diff --git a/statviz/src/views/Statistics/components/TopProducts.tsx b/statviz/src/views/Statistics/components/TopProducts.tsx index 641f9dade..7d6b2c6ab 100644 --- a/statviz/src/views/Statistics/components/TopProducts.tsx +++ b/statviz/src/views/Statistics/components/TopProducts.tsx @@ -103,7 +103,11 @@ export default function TopProducts(params: { } return ( - + { - const chart = params.custom - ? document.getElementById(params.visId) + const [isLoading, setLoading] = useState(false); + + const downloadImage = () => { + const chart = document.getElementById(params.visId); + + domtoimage.toJpeg(chart, { quality: 0.9 }).then((dataUrl) => { + const a = document.createElement("a"); + a.setAttribute("href", dataUrl); + + a.setAttribute("download", params.visId); + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + setLoading(false); + }); + }; + + const downloadImageSVG = () => { + const svgData = params.custom + ? document.getElementById(params.visId).outerHTML : document.getElementById(params.visId)?.firstChild?.firstChild - ?.firstChild; + ?.firstChild.outerHTML; + const svgBlob = new Blob([svgData], { + type: "image/svg+xml;charset=utf-8", + }); + const svgUrl = URL.createObjectURL(svgBlob); + const downloadLink = document.createElement("a"); + downloadLink.href = svgUrl; + downloadLink.download = "newesttree.svg"; + document.body.appendChild(downloadLink); + downloadLink.click(); + document.body.removeChild(downloadLink); + }; - const serializer = new XMLSerializer(); - let source = serializer.serializeToString(chart); + const download = () => { + setLoading(true); + setTimeout(downloadImage, 1); + }; - //add name spaces. - if (!source.match(/^]+xmlns="http\:\/\/www\.w3\.org\/2000\/svg"/)) { - source = source.replace( - /^]+"http\:\/\/www\.w3\.org\/1999\/xlink"/)) { - source = source.replace( - /^ { + const marginInPx = 50; + if (typeof params.maxWidthPx === "string") { + return parseInt(params.maxWidthPx) + marginInPx; } - source = '\r\n' + source; - - const a = document.createElement("a"); - a.setAttribute( - "href", - "data:image/svg+xml;charset=utf-8," + encodeURIComponent(source) - ); - - a.setAttribute("download", params.visId + ".svg"); - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); + return params.maxWidthPx + marginInPx; }; return ( - - - {params.heading} - - - + + + + + {params.heading} + + + + Download + + + + + + + + + Width + + + + + + + + + + Height + + + + + + + + + +
+ + + Options + + + Heading + + + Timestamp + + + + +
+ +
+ + Downloads + + + + + +
+ + + + + ); } diff --git a/statviz/yarn.lock b/statviz/yarn.lock index 44581e9c4..96974b835 100644 --- a/statviz/yarn.lock +++ b/statviz/yarn.lock @@ -3755,6 +3755,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-to-image@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/dom-to-image/-/dom-to-image-2.6.0.tgz#8a503608088c87b1c22f9034ae032e1898955867" + integrity sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA== + dot-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" From f1bd2c05034229e8a43306965f63dbc5c19e9630 Mon Sep 17 00:00:00 2001 From: MaikNeubert Date: Sat, 4 Nov 2023 11:11:02 +0100 Subject: [PATCH 003/178] wip image export & move components from view to components folder --- statviz/package.json | 2 +- statviz/src/App.tsx | 4 +- .../Form}/TimeRangeSelect.tsx | 2 +- .../Statistics => }/components/NoDataCard.tsx | 0 .../Statistics => }/components/VisHeader.tsx | 70 +++++++--- .../src/components/nivo-graphs/BarChart.tsx | 24 ++-- .../visualizations}/BoxFlowSankey.tsx | 4 +- .../visualizations}/CreatedBoxes.tsx | 27 +++- .../visualizations}/DemographicChart.tsx | 10 +- .../visualizations}/TopProducts.tsx | 63 +++++++-- .../src/{utils => }/hooks/useCreatedBoxes.ts | 4 +- statviz/src/hooks/useExport.ts | 36 +++++ statviz/src/utils/chart.ts | 2 + statviz/src/utils/theme.ts | 125 ++++++++++++++++++ statviz/src/views/Dashboard/Dashboard.tsx | 2 +- statviz/src/views/Dashboard/ItemsAndBoxes.tsx | 4 +- statviz/src/views/Dashboard/MovedBoxes.tsx | 2 +- .../src/views/Statistics/DemographicView.tsx | 2 +- statviz/yarn.lock | 8 +- 19 files changed, 326 insertions(+), 65 deletions(-) rename statviz/src/{views/Statistics/components/filter => components/Form}/TimeRangeSelect.tsx (98%) rename statviz/src/{views/Statistics => }/components/NoDataCard.tsx (100%) rename statviz/src/{views/Statistics => }/components/VisHeader.tsx (68%) rename statviz/src/{views/Statistics/components => components/visualizations}/BoxFlowSankey.tsx (71%) rename statviz/src/{views/Statistics/components => components/visualizations}/CreatedBoxes.tsx (74%) rename statviz/src/{views/Statistics/components => components/visualizations}/DemographicChart.tsx (88%) rename statviz/src/{views/Statistics/components => components/visualizations}/TopProducts.tsx (64%) rename statviz/src/{utils => }/hooks/useCreatedBoxes.ts (91%) create mode 100644 statviz/src/hooks/useExport.ts diff --git a/statviz/package.json b/statviz/package.json index 447082a03..2c7134439 100644 --- a/statviz/package.json +++ b/statviz/package.json @@ -39,7 +39,7 @@ "react-hook-form": "^7.43.1", "react-router-dom": "^6.15.0", "zod": "^3.22.3", - "dom-to-image": "^2.6.0" + "dom-to-image-more": "^3.2.0" }, "devDependencies": { "@graphql-codegen/cli": "5.0.0", diff --git a/statviz/src/App.tsx b/statviz/src/App.tsx index 1f14c336d..00b905a1a 100644 --- a/statviz/src/App.tsx +++ b/statviz/src/App.tsx @@ -1,8 +1,8 @@ import { Route, Routes } from "react-router-dom"; import DemographicView from "./views/Statistics/DemographicView"; import Dashboard from "./views/Dashboard/Dashboard"; -import CreatedBoxes from "./views/Statistics/components/CreatedBoxes"; -import TopProducts from "./views/Statistics/components/TopProducts"; +import CreatedBoxes from "./components/visualizations/CreatedBoxes"; +import TopProducts from "./components/visualizations/TopProducts"; function App() { return ( diff --git a/statviz/src/views/Statistics/components/filter/TimeRangeSelect.tsx b/statviz/src/components/Form/TimeRangeSelect.tsx similarity index 98% rename from statviz/src/views/Statistics/components/filter/TimeRangeSelect.tsx rename to statviz/src/components/Form/TimeRangeSelect.tsx index 8907bbd91..910e6670d 100644 --- a/statviz/src/views/Statistics/components/filter/TimeRangeSelect.tsx +++ b/statviz/src/components/Form/TimeRangeSelect.tsx @@ -1,5 +1,5 @@ import { Box, Button, ButtonGroup, HStack, Heading } from "@chakra-ui/react"; -import DateField from "../../../../components/Form/DateField"; +import DateField from "./DateField"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod/src/zod.js"; import { useForm } from "react-hook-form"; diff --git a/statviz/src/views/Statistics/components/NoDataCard.tsx b/statviz/src/components/NoDataCard.tsx similarity index 100% rename from statviz/src/views/Statistics/components/NoDataCard.tsx rename to statviz/src/components/NoDataCard.tsx diff --git a/statviz/src/views/Statistics/components/VisHeader.tsx b/statviz/src/components/VisHeader.tsx similarity index 68% rename from statviz/src/views/Statistics/components/VisHeader.tsx rename to statviz/src/components/VisHeader.tsx index 5666acc29..52ccfa8bd 100644 --- a/statviz/src/views/Statistics/components/VisHeader.tsx +++ b/statviz/src/components/VisHeader.tsx @@ -24,30 +24,53 @@ import { Wrap, } from "@chakra-ui/react"; import { DownloadIcon } from "@chakra-ui/icons"; -import domtoimage from "dom-to-image"; +import domtoimage from "dom-to-image-more"; import { useState } from "react"; export default function VisHeader(params: { heading: string; visId: string; maxWidthPx: number | string; + onExport: ( + width: number, + height: number, + includeHeading: boolean, + includeTimestamp: boolean + ) => void; + onExportFinished: () => void; custom?: boolean; }) { const [isLoading, setLoading] = useState(false); + const [inputWidth, setInputWidth] = useState(800); + const [inputHeight, setInputHeight] = useState(500); + const [includeHeading, setIncludeHeading] = useState(true); + const [includeTimestamp, setIncludeTimestamp] = useState(true); + + const handleIncludeHeading = (e) => setIncludeHeading(e.target.checked); + const handleIncludeTimestamp = (e) => setIncludeTimestamp(e.target.checked); const downloadImage = () => { - const chart = document.getElementById(params.visId); + const chart = document.getElementById(params.visId); // params.visId - domtoimage.toJpeg(chart, { quality: 0.9 }).then((dataUrl) => { - const a = document.createElement("a"); - a.setAttribute("href", dataUrl); + domtoimage + .toJpeg(chart, { + quality: 0.9, + width: inputWidth, + height: inputHeight, + bgColor: "#ffffff", + }) + .then((dataUrl) => { + console.log(dataUrl); + const a = document.createElement("a"); + a.setAttribute("href", dataUrl); - a.setAttribute("download", params.visId); - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - setLoading(false); - }); + a.setAttribute("download", params.visId); + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + setLoading(false); + params.onExportFinished(); + }); }; const downloadImageSVG = () => { @@ -61,15 +84,18 @@ export default function VisHeader(params: { const svgUrl = URL.createObjectURL(svgBlob); const downloadLink = document.createElement("a"); downloadLink.href = svgUrl; - downloadLink.download = "newesttree.svg"; + downloadLink.download = params.visId + ".svg"; document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); }; const download = () => { + params.onExport(inputWidth, inputHeight, includeHeading, includeHeading); setLoading(true); - setTimeout(downloadImage, 1); + // timeout triggers the rerender with loading animations before generating the image. + // without the timeout the loading animation sometimes won't be triggered + setTimeout(downloadImage, 250); }; const getMaxWidth = () => { @@ -102,9 +128,10 @@ export default function VisHeader(params: { @@ -118,9 +145,10 @@ export default function VisHeader(params: { @@ -135,10 +163,18 @@ export default function VisHeader(params: { Options - + Heading - + Timestamp diff --git a/statviz/src/components/nivo-graphs/BarChart.tsx b/statviz/src/components/nivo-graphs/BarChart.tsx index b51b9f6c7..9c0617470 100644 --- a/statviz/src/components/nivo-graphs/BarChart.tsx +++ b/statviz/src/components/nivo-graphs/BarChart.tsx @@ -1,5 +1,6 @@ -import { ResponsiveBar } from "@nivo/bar"; -import { nivoScheme } from "../../utils/theme"; +import { ResponsiveBar, ResponsiveBarCanvas } from "@nivo/bar"; +import { nivoScheme, scaleTick, scaledTheme } from "../../utils/theme"; +import { percent, pixelCalculator } from "../../utils/chart"; export interface BarChart { width: string; @@ -7,6 +8,7 @@ export interface BarChart { data: Array; visId: string; keys?: Array; + animate?: boolean; // null defaults to true indexBy?: string; ariaLabel?: string; legend?: boolean; @@ -51,12 +53,18 @@ export default function BarChart(barChart: BarChart) { (CREATED_BOXES_QUERY, { variables: { baseId: parseInt(baseId) } }); const createdBoxes = useCreatedBoxes(data); + const { exportWidth, exportHeight, isExporting, onExport, onExportFinish } = + useExport(); + const getChartData = () => { if (data === undefined) return []; @@ -97,6 +101,17 @@ export default function CreatedBoxes(params: { height={params.height} /> + {isExporting && ( + + )} ); } diff --git a/statviz/src/views/Statistics/components/DemographicChart.tsx b/statviz/src/components/visualizations/DemographicChart.tsx similarity index 88% rename from statviz/src/views/Statistics/components/DemographicChart.tsx rename to statviz/src/components/visualizations/DemographicChart.tsx index 8e3de51d5..4c63f2d8d 100644 --- a/statviz/src/views/Statistics/components/DemographicChart.tsx +++ b/statviz/src/components/visualizations/DemographicChart.tsx @@ -1,11 +1,11 @@ import { Card, CardBody, CardHeader, Heading } from "@chakra-ui/react"; -import BarChartCenterAxis from "../../../components/custom-graphs/BarChartCenterAxis"; +import BarChartCenterAxis from "../custom-graphs/BarChartCenterAxis"; import { range } from "lodash"; -import { HumanGender } from "../../../types/generated/graphql"; -import { getSelectionBackground } from "../../../utils/theme"; +import { HumanGender } from "../../types/generated/graphql"; +import { getSelectionBackground } from "../../utils/theme"; import { useState } from "react"; -import VisHeader from "./VisHeader"; -import { table } from "../../../utils/table"; +import VisHeader from "../VisHeader"; +import { table } from "../../utils/table"; export interface IDemographicFact { createdOn: Date; diff --git a/statviz/src/views/Statistics/components/TopProducts.tsx b/statviz/src/components/visualizations/TopProducts.tsx similarity index 64% rename from statviz/src/views/Statistics/components/TopProducts.tsx rename to statviz/src/components/visualizations/TopProducts.tsx index 7d6b2c6ab..d887017ce 100644 --- a/statviz/src/views/Statistics/components/TopProducts.tsx +++ b/statviz/src/components/visualizations/TopProducts.tsx @@ -1,21 +1,20 @@ -import BarChart from "../../../components/nivo-graphs/BarChart"; -import { Sort, table } from "../../../utils/table"; +import BarChart from "../nivo-graphs/BarChart"; +import { Sort, table } from "../../utils/table"; import { CreatedBoxesData, - CreatedBoxesResult, ProductDimensionInfo, QueryCreatedBoxesArgs, -} from "../../../types/generated/graphql"; +} from "../../types/generated/graphql"; import { ApolloError, useQuery, gql } from "@apollo/client"; -import { Card, CardBody, CardHeader, Heading } from "@chakra-ui/react"; +import { Box, Card, CardBody, Heading } from "@chakra-ui/react"; import { round } from "lodash"; -import { useMemo, useState } from "react"; +import { useMemo } from "react"; import { useParams } from "react-router-dom"; -import { BoxesOrItemsCount } from "../../Dashboard/Dashboard"; -import { getSelectionBackground } from "../../../utils/theme"; -import VisHeader from "./VisHeader"; -import useCreatedBoxes from "../../../utils/hooks/useCreatedBoxes"; -import NoDataCard from "./NoDataCard"; +import { BoxesOrItemsCount } from "../../views/Dashboard/Dashboard"; +import VisHeader from "../VisHeader"; +import useCreatedBoxes from "../../hooks/useCreatedBoxes"; +import NoDataCard from "../NoDataCard"; +import useExport from "../../hooks/useExport"; const CREATED_BOXES_QUERY = gql` query createdBoxes($baseId: Int!) { @@ -57,6 +56,16 @@ export default function TopProducts(params: { >(CREATED_BOXES_QUERY, { variables: { baseId: parseInt(baseId) } }); const createdBoxes = useCreatedBoxes(data); + const { + exportWidth, + exportHeight, + isExporting, + exportHeading, + exportTimestamp, + onExport, + onExportFinish, + } = useExport(); + const getChartData = () => { if (data === undefined) { return []; @@ -101,21 +110,51 @@ export default function TopProducts(params: { if (chartData.length == 0) { return ; } + return ( + {isExporting && ( + + {exportHeading && ( + + {heading} + + )} + {exportTimestamp && ( + {new Date().toISOString()} + )} + + + )} ); } diff --git a/statviz/src/utils/hooks/useCreatedBoxes.ts b/statviz/src/hooks/useCreatedBoxes.ts similarity index 91% rename from statviz/src/utils/hooks/useCreatedBoxes.ts rename to statviz/src/hooks/useCreatedBoxes.ts index 472d62440..cb870bdcb 100644 --- a/statviz/src/utils/hooks/useCreatedBoxes.ts +++ b/statviz/src/hooks/useCreatedBoxes.ts @@ -2,9 +2,9 @@ import { gql, useQuery } from "@apollo/client"; import { CreatedBoxesData, CreatedBoxesResult, -} from "../../types/generated/graphql"; +} from "../types/generated/graphql"; import { useMemo } from "react"; -import { createdBoxesTable } from "../table"; +import { createdBoxesTable } from "../utils/table"; import { useSearchParams } from "react-router-dom"; export default function useCreatedBoxes(data?: { diff --git a/statviz/src/hooks/useExport.ts b/statviz/src/hooks/useExport.ts new file mode 100644 index 000000000..ab26c1d67 --- /dev/null +++ b/statviz/src/hooks/useExport.ts @@ -0,0 +1,36 @@ +import { useState } from "react"; + +export default function useExport() { + const [exportWidth, setExportWidth] = useState(0); + const [exportHeight, setExportHeight] = useState(0); + const [exportHeading, setExportHeading] = useState(false); + const [exportTimestamp, setExportTimestamp] = useState(false); + const [isExporting, setIsExporting] = useState(false); + + const onExport = ( + width: number, + height: number, + exportHeading: boolean, + exportTimestamp: boolean + ) => { + setExportHeading(exportHeading); + setExportTimestamp(exportTimestamp); + setExportWidth(width); + setExportHeight(height); + setIsExporting(true); + }; + + const onExportFinish = () => { + setIsExporting(false); + }; + + return { + exportWidth, + exportHeight, + exportHeading, + exportTimestamp, + isExporting, + onExport, + onExportFinish, + }; +} diff --git a/statviz/src/utils/chart.ts b/statviz/src/utils/chart.ts index 8210a8b61..db1d87bc7 100644 --- a/statviz/src/utils/chart.ts +++ b/statviz/src/utils/chart.ts @@ -22,3 +22,5 @@ export const labelProps = { fontFamily: "Open Sans", fontSize: 16, }; + +export const percent = (px: number, percent: number) => px * (percent / 100); diff --git a/statviz/src/utils/theme.ts b/statviz/src/utils/theme.ts index 7e8bde150..bed313c7e 100644 --- a/statviz/src/utils/theme.ts +++ b/statviz/src/utils/theme.ts @@ -213,3 +213,128 @@ export const nivoScheme: Theme = { tableCellValue: {}, }, }; + +export const scaledTheme = (width: number, height: number): Theme => { + const strokeWidth = Math.floor(height / 500) + 1; + const fontSizeAxis = Math.floor(height / 35) + 1; + const fontSizeLegend = Math.floor(height / 20) + 1; + const fontSizeText = Math.floor(height / 20) + 1; + const fontSizeLabel = Math.floor(width / 25) + 1; + + return { + background: "#ffffff", + fontFamily: "Open Sans", + text: { + fontSize: fontSizeText, + fill: "#333333", + outlineWidth: 0, + outlineColor: "transparent", + }, + labels: { + text: { + fontSize: fontSizeLabel, + }, + }, + axis: { + domain: { + line: { + stroke: "#777777", + strokeWidth: strokeWidth, + }, + }, + legend: { + text: { + fontSize: fontSizeLegend, + fill: "#333333", + outlineWidth: 0, + outlineColor: "transparent", + }, + }, + ticks: { + line: { + stroke: "#777777", + strokeWidth: strokeWidth, + }, + text: { + fontSize: fontSizeAxis, + fill: "#333333", + outlineWidth: 0, + outlineColor: "transparent", + }, + }, + }, + grid: { + line: { + stroke: "#dddddd", + strokeWidth: strokeWidth, + }, + }, + legends: { + title: { + text: { + fontSize: fontSizeLegend, + fill: "#333333", + outlineWidth: 0, + outlineColor: "transparent", + }, + }, + text: { + fontSize: fontSizeLegend, + fill: "#333333", + outlineWidth: 0, + outlineColor: "transparent", + }, + ticks: { + line: {}, + text: { + fontSize: fontSizeLegend, + fill: "#333333", + outlineWidth: 0, + outlineColor: "transparent", + }, + }, + }, + annotations: { + text: { + fontSize: fontSizeLegend, + fill: "#333333", + outlineWidth: 2, + outlineColor: "#ffffff", + outlineOpacity: 1, + }, + link: { + stroke: "#000000", + strokeWidth: 1, + outlineWidth: 2, + outlineColor: "#ffffff", + outlineOpacity: 1, + }, + outline: { + stroke: "#000000", + strokeWidth: 2, + outlineWidth: 2, + outlineColor: "#ffffff", + outlineOpacity: 1, + }, + symbol: { + fill: "#000000", + outlineWidth: 2, + outlineColor: "#ffffff", + outlineOpacity: 1, + }, + }, + tooltip: { + container: { + background: "#ffffff", + fontSize: fontSizeLegend, + }, + basic: {}, + chip: {}, + table: {}, + tableCell: {}, + tableCellValue: {}, + }, + }; +}; + +export const scaleTick = (height: number) => height / 80; diff --git a/statviz/src/views/Dashboard/Dashboard.tsx b/statviz/src/views/Dashboard/Dashboard.tsx index 5721dd311..c7baa7ad6 100644 --- a/statviz/src/views/Dashboard/Dashboard.tsx +++ b/statviz/src/views/Dashboard/Dashboard.tsx @@ -8,7 +8,7 @@ import { Wrap, WrapItem, } from "@chakra-ui/react"; -import TimeRangeSelect from "../Statistics/components/filter/TimeRangeSelect"; +import TimeRangeSelect from "../../components/Form/TimeRangeSelect"; import { useSearchParams } from "react-router-dom"; import Demographics from "./Demographics"; import MovedBoxes from "./MovedBoxes"; diff --git a/statviz/src/views/Dashboard/ItemsAndBoxes.tsx b/statviz/src/views/Dashboard/ItemsAndBoxes.tsx index 43709d4db..d7ab778ba 100644 --- a/statviz/src/views/Dashboard/ItemsAndBoxes.tsx +++ b/statviz/src/views/Dashboard/ItemsAndBoxes.tsx @@ -12,8 +12,8 @@ import { } from "@chakra-ui/react"; import { useSearchParams } from "react-router-dom"; import { useState, useEffect } from "react"; -import TopProductsPieChart from "../Statistics/components/TopProducts"; -import CreatedBoxesBarChart from "../Statistics/components/CreatedBoxes"; +import TopProductsPieChart from "../../components/visualizations/TopProducts"; +import CreatedBoxesBarChart from "../../components/visualizations/CreatedBoxes"; export type BoxesOrItemsCount = "boxesCount" | "itemsCount"; export const isBoxesOrItemsCount = ( diff --git a/statviz/src/views/Dashboard/MovedBoxes.tsx b/statviz/src/views/Dashboard/MovedBoxes.tsx index bda329cc8..ffb2db2c4 100644 --- a/statviz/src/views/Dashboard/MovedBoxes.tsx +++ b/statviz/src/views/Dashboard/MovedBoxes.tsx @@ -8,7 +8,7 @@ import { WrapItem, Box, } from "@chakra-ui/react"; -import BoxFlowSankey from "../Statistics/components/BoxFlowSankey"; +import BoxFlowSankey from "../../components/visualizations/BoxFlowSankey"; export default function MovedBoxes() { return ( diff --git a/statviz/src/views/Statistics/DemographicView.tsx b/statviz/src/views/Statistics/DemographicView.tsx index 26c4d69af..2d90f2511 100644 --- a/statviz/src/views/Statistics/DemographicView.tsx +++ b/statviz/src/views/Statistics/DemographicView.tsx @@ -3,7 +3,7 @@ import { BeneficiaryDemographicsQuery, BeneficiaryDemographicsQueryVariables, } from "../../types/generated/graphql"; -import DemographicChart from "./components/DemographicChart"; +import DemographicChart from "../../components/visualizations/DemographicChart"; import { useParams } from "react-router-dom"; import { Card, CardBody, CardHeader, Heading } from "@chakra-ui/react"; diff --git a/statviz/yarn.lock b/statviz/yarn.lock index 96974b835..ebf57a320 100644 --- a/statviz/yarn.lock +++ b/statviz/yarn.lock @@ -3755,10 +3755,10 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-to-image@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/dom-to-image/-/dom-to-image-2.6.0.tgz#8a503608088c87b1c22f9034ae032e1898955867" - integrity sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA== +dom-to-image-more@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/dom-to-image-more/-/dom-to-image-more-3.2.0.tgz#8bc4e0299bb2da9d27d77b89910a4cfd65de49be" + integrity sha512-2bGQTB6m17MBseVhIjShwZqqqCyVS9GgTykWqvVXMqr56fSgHhXnEvZfZkaSuHJYW3ICZQ3sZwAu+UY5tfsF9Q== dot-case@^3.0.4: version "3.0.4" From 147e85b901eebc80b90fd7395cdeec102d6427d3 Mon Sep 17 00:00:00 2001 From: MaikNeubert Date: Sat, 4 Nov 2023 12:38:37 +0100 Subject: [PATCH 004/178] jpg export for top products (margins of additional info wip) --- .../src/components/Form/TimeRangeSelect.tsx | 8 +- statviz/src/components/VisHeader.tsx | 21 ++++- .../src/components/nivo-graphs/BarChart.tsx | 74 +++++++++++++-- .../visualizations/CreatedBoxes.tsx | 39 +------- .../components/visualizations/TopProducts.tsx | 48 ++-------- statviz/src/hooks/useCreatedBoxes.ts | 93 ++++++++++++++----- statviz/src/hooks/useExport.ts | 6 +- statviz/src/utils/chart.ts | 2 + 8 files changed, 175 insertions(+), 116 deletions(-) diff --git a/statviz/src/components/Form/TimeRangeSelect.tsx b/statviz/src/components/Form/TimeRangeSelect.tsx index 910e6670d..ffd9115dd 100644 --- a/statviz/src/components/Form/TimeRangeSelect.tsx +++ b/statviz/src/components/Form/TimeRangeSelect.tsx @@ -1,11 +1,12 @@ -import { Box, Button, ButtonGroup, HStack, Heading } from "@chakra-ui/react"; +import { Box, HStack } from "@chakra-ui/react"; import DateField from "./DateField"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod/src/zod.js"; import { useForm } from "react-hook-form"; -import { addDays, isBefore, subDays, subMonths } from "date-fns"; +import { subMonths } from "date-fns"; import { useEffect } from "react"; import { useSearchParams } from "react-router-dom"; +import { date2String } from "../../utils/chart"; export const FilterCreatedOnFormScheme = z.object({ from: z @@ -37,9 +38,6 @@ export default function TimeRangeSelect(params: { }); const [searchParams, setSearchParams] = useSearchParams(); - - const date2String = (date: Date) => date.toISOString().substring(0, 10); - const toFormValue = watch("to"); const fromFormValue = watch("from"); diff --git a/statviz/src/components/VisHeader.tsx b/statviz/src/components/VisHeader.tsx index 52ccfa8bd..1b23dc549 100644 --- a/statviz/src/components/VisHeader.tsx +++ b/statviz/src/components/VisHeader.tsx @@ -35,7 +35,8 @@ export default function VisHeader(params: { width: number, height: number, includeHeading: boolean, - includeTimestamp: boolean + includeTimestamp: boolean, + includeFromTo: boolean ) => void; onExportFinished: () => void; custom?: boolean; @@ -45,9 +46,11 @@ export default function VisHeader(params: { const [inputHeight, setInputHeight] = useState(500); const [includeHeading, setIncludeHeading] = useState(true); const [includeTimestamp, setIncludeTimestamp] = useState(true); + const [includeFromTo, setIncludeFromTo] = useState(true); const handleIncludeHeading = (e) => setIncludeHeading(e.target.checked); const handleIncludeTimestamp = (e) => setIncludeTimestamp(e.target.checked); + const handleIncludeFromTo = (e) => setIncludeFromTo(e.target.checked); const downloadImage = () => { const chart = document.getElementById(params.visId); // params.visId @@ -60,7 +63,6 @@ export default function VisHeader(params: { bgColor: "#ffffff", }) .then((dataUrl) => { - console.log(dataUrl); const a = document.createElement("a"); a.setAttribute("href", dataUrl); @@ -91,7 +93,13 @@ export default function VisHeader(params: { }; const download = () => { - params.onExport(inputWidth, inputHeight, includeHeading, includeHeading); + params.onExport( + inputWidth, + inputHeight, + includeHeading, + includeTimestamp, + includeFromTo + ); setLoading(true); // timeout triggers the rerender with loading animations before generating the image. // without the timeout the loading animation sometimes won't be triggered @@ -170,6 +178,13 @@ export default function VisHeader(params: { > Heading + + timerange + ; visId: string; + heading?: string | false; + timestamp?: string | false; + timeRange?: string | false; keys?: Array; animate?: boolean; // null defaults to true indexBy?: string; @@ -17,6 +21,55 @@ export interface BarChart { } export default function BarChart(barChart: BarChart) { + const height = parseInt(barChart.height); + const width = parseInt(barChart.width); + + const theme = scaledTheme(width, height); + // getting updated depending on how much space is needed for extra information e. g. Timestamp and Heading + let marginTop = percent(height, 5); + let marginBottom = percent(height, 25); + + const layers: BarLayer[] = [ + "grid", + "axes", + "bars", + "markers", + "legends", + "annotations", + ]; + + if (typeof barChart.heading === "string") { + marginTop += 50; + layers.push(() => { + return ( + + {barChart.heading} + + ); + }); + } + if (typeof barChart.timeRange === "string") { + marginTop += 20; + layers.push(() => { + return ( + + {barChart.timeRange} + + ); + }); + } + if (typeof barChart.timestamp === "string") { + marginBottom += 20; + const y = height - marginTop - 20; + layers.push(() => { + return ( + + {barChart.timestamp} + + ); + }); + } + const legend = barChart.legend === true ? [ @@ -56,15 +109,16 @@ export default function BarChart(barChart: BarChart) { animate={barChart.animate === true || barChart.animate === null} indexBy={barChart.indexBy} margin={{ - top: percent(parseInt(barChart.height), 5), - right: percent(parseInt(barChart.width), 10), - bottom: percent(parseInt(barChart.height), 20), - left: percent(parseInt(barChart.width), 15), + top: marginTop, + right: percent(width, 10), + bottom: marginBottom, + left: percent(width, 15), }} + layers={layers} padding={0.3} valueScale={{ type: "linear" }} indexScale={{ type: "band", round: true }} - theme={scaledTheme(parseInt(barChart.width), parseInt(barChart.height))} + theme={theme} colors="#ec5063" defs={[ { @@ -93,16 +147,16 @@ export default function BarChart(barChart: BarChart) { axisTop={null} axisRight={null} axisBottom={{ - tickSize: scaleTick(parseInt(barChart.height)), - tickPadding: scaleTick(parseInt(barChart.height)), + tickSize: scaleTick(height), + tickPadding: scaleTick(height), tickRotation: 25, legend: barChart.labelAxisBottom, legendPosition: "middle", legendOffset: 32, }} axisLeft={{ - tickSize: scaleTick(parseInt(barChart.height)), - tickPadding: scaleTick(parseInt(barChart.height)), + tickSize: scaleTick(height), + tickPadding: scaleTick(height), tickRotation: 0, legend: barChart.labelAxisLeft, legendPosition: "middle", diff --git a/statviz/src/components/visualizations/CreatedBoxes.tsx b/statviz/src/components/visualizations/CreatedBoxes.tsx index 731bca391..8fab0883f 100644 --- a/statviz/src/components/visualizations/CreatedBoxes.tsx +++ b/statviz/src/components/visualizations/CreatedBoxes.tsx @@ -1,45 +1,15 @@ -import { ApolloError, useQuery, gql } from "@apollo/client"; +import { ApolloError } from "@apollo/client"; import { Card, CardBody } from "@chakra-ui/react"; import _ from "lodash"; -import { - CreatedBoxesData, - QueryCreatedBoxesArgs, -} from "../../types/generated/graphql"; import BarChart from "../nivo-graphs/BarChart"; import { useMemo } from "react"; import useCreatedBoxes from "../../hooks/useCreatedBoxes"; -import { useParams } from "react-router-dom"; import { BoxesOrItemsCount } from "../../views/Dashboard/Dashboard"; import VisHeader from "../VisHeader"; import NoDataCard from "../NoDataCard"; import useExport from "../../hooks/useExport"; -const CREATED_BOXES_QUERY = gql` - query createdBoxes($baseId: Int!) { - createdBoxes(baseId: $baseId) { - facts { - boxesCount - productId - categoryId - createdOn - gender - itemsCount - } - dimensions { - product { - id - name - } - category { - id - name - } - } - } - } -`; - const visId = "created-boxes"; export default function CreatedBoxes(params: { @@ -47,12 +17,7 @@ export default function CreatedBoxes(params: { height: string; boxesOrItems: BoxesOrItemsCount; }) { - const { baseId } = useParams(); - const { data, loading, error } = useQuery< - CreatedBoxesData, - QueryCreatedBoxesArgs - >(CREATED_BOXES_QUERY, { variables: { baseId: parseInt(baseId) } }); - const createdBoxes = useCreatedBoxes(data); + const { createdBoxes, loading, data, error } = useCreatedBoxes(); const { exportWidth, exportHeight, isExporting, onExport, onExportFinish } = useExport(); diff --git a/statviz/src/components/visualizations/TopProducts.tsx b/statviz/src/components/visualizations/TopProducts.tsx index d887017ce..173ea0a7c 100644 --- a/statviz/src/components/visualizations/TopProducts.tsx +++ b/statviz/src/components/visualizations/TopProducts.tsx @@ -16,31 +16,6 @@ import useCreatedBoxes from "../../hooks/useCreatedBoxes"; import NoDataCard from "../NoDataCard"; import useExport from "../../hooks/useExport"; -const CREATED_BOXES_QUERY = gql` - query createdBoxes($baseId: Int!) { - createdBoxes(baseId: $baseId) { - facts { - boxesCount - productId - categoryId - createdOn - gender - itemsCount - } - dimensions { - product { - id - name - } - category { - id - name - } - } - } - } -`; - const visId = "top-products"; export default function TopProducts(params: { @@ -49,12 +24,8 @@ export default function TopProducts(params: { boxesOrItems: BoxesOrItemsCount; }) { const boxesOrItems = params.boxesOrItems; - const { baseId } = useParams(); - const { data, loading, error } = useQuery< - CreatedBoxesData, - QueryCreatedBoxesArgs - >(CREATED_BOXES_QUERY, { variables: { baseId: parseInt(baseId) } }); - const createdBoxes = useCreatedBoxes(data); + const { createdBoxes, data, loading, error, fromToTimestamp } = + useCreatedBoxes(); const { exportWidth, @@ -62,6 +33,7 @@ export default function TopProducts(params: { isExporting, exportHeading, exportTimestamp, + exportFromTo, onExport, onExportFinish, } = useExport(); @@ -124,6 +96,9 @@ export default function TopProducts(params: { @@ -138,17 +113,12 @@ export default function TopProducts(params: { left="-5000" id={visId} > - {exportHeading && ( - - {heading} - - )} - {exportTimestamp && ( - {new Date().toISOString()} - )} { - if (!data) return createdBoxesTable([]); +export default function useCreatedBoxes() { + const { baseId } = useParams(); + const { data, loading, error } = useQuery< + CreatedBoxesData, + QueryCreatedBoxesArgs + >(CREATED_BOXES_QUERY, { variables: { baseId: parseInt(baseId) } }); - const boxesFacts = createdBoxesTable( - data.createdBoxes.facts as CreatedBoxesResult[] - ); + const [searchParams] = useSearchParams(); - const fromToInterval = { + const fromToInterval = useMemo( + () => ({ start: new Date(searchParams.get("from") as string), end: new Date(searchParams.get("to") as string), - }; - - try { - const filteredByTime = boxesFacts.filterCreatedOn(fromToInterval); - return createdBoxesTable(filteredByTime.data); - } catch (e) { - console.log("invalid timerange"); - return createdBoxesTable([]); - } - }, [data, searchParams]); + }), + [searchParams] + ); + + const fromToTimestamp = useMemo( + () => + "from " + + date2String(fromToInterval.start) + + " to " + + date2String(fromToInterval.end), + [fromToInterval] + ); + + return { + createdBoxes: useMemo(() => { + if (!data) return createdBoxesTable([]); + + const boxesFacts = createdBoxesTable( + data.createdBoxes.facts as CreatedBoxesResult[] + ); + + try { + const filteredByTime = boxesFacts.filterCreatedOn(fromToInterval); + return createdBoxesTable(filteredByTime.data); + } catch (e) { + console.log("invalid timerange"); + return createdBoxesTable([]); + } + }, [data, fromToInterval]), + loading, + error, + data, + baseId, + fromToInterval, + fromToTimestamp, + }; } diff --git a/statviz/src/hooks/useExport.ts b/statviz/src/hooks/useExport.ts index ab26c1d67..26da3a7e9 100644 --- a/statviz/src/hooks/useExport.ts +++ b/statviz/src/hooks/useExport.ts @@ -5,16 +5,19 @@ export default function useExport() { const [exportHeight, setExportHeight] = useState(0); const [exportHeading, setExportHeading] = useState(false); const [exportTimestamp, setExportTimestamp] = useState(false); + const [exportFromTo, setExportFromTo] = useState(false); const [isExporting, setIsExporting] = useState(false); const onExport = ( width: number, height: number, exportHeading: boolean, - exportTimestamp: boolean + exportTimestamp: boolean, + exportFromTo: boolean ) => { setExportHeading(exportHeading); setExportTimestamp(exportTimestamp); + setExportFromTo(exportFromTo); setExportWidth(width); setExportHeight(height); setIsExporting(true); @@ -29,6 +32,7 @@ export default function useExport() { exportHeight, exportHeading, exportTimestamp, + exportFromTo, isExporting, onExport, onExportFinish, diff --git a/statviz/src/utils/chart.ts b/statviz/src/utils/chart.ts index db1d87bc7..0042165e9 100644 --- a/statviz/src/utils/chart.ts +++ b/statviz/src/utils/chart.ts @@ -24,3 +24,5 @@ export const labelProps = { }; export const percent = (px: number, percent: number) => px * (percent / 100); + +export const date2String = (date: Date) => date.toISOString().substring(0, 10); From dac4006a450fca3a8f2dfcd14cd731d2ac83a0c2 Mon Sep 17 00:00:00 2001 From: MaikNeubert Date: Sat, 4 Nov 2023 17:45:50 +0100 Subject: [PATCH 005/178] update Demographic Chart, statviz refactoring --- statviz/src/App.tsx | 3 +- statviz/src/components/VisHeader.tsx | 62 +++++----- .../custom-graphs/BarChartCenterAxis.tsx | 36 +++--- .../src/components/nivo-graphs/BarChart.tsx | 47 ++++++-- .../visualizations/CreatedBoxes.tsx | 51 ++++++--- .../visualizations/DemographicChart.tsx | 108 +++++++++++------- .../components/visualizations/TopProducts.tsx | 33 ++---- statviz/src/hooks/useCreatedBoxes.ts | 33 ++---- statviz/src/hooks/useDemographics.ts | 59 ++++++++++ statviz/src/hooks/useExport.ts | 10 +- statviz/src/hooks/useTimerange.ts | 26 +++++ statviz/src/utils/table.ts | 20 +++- statviz/src/utils/theme.ts | 4 +- statviz/src/views/Dashboard/Demographics.tsx | 4 +- .../src/views/Statistics/DemographicView.tsx | 53 --------- 15 files changed, 316 insertions(+), 233 deletions(-) create mode 100644 statviz/src/hooks/useDemographics.ts create mode 100644 statviz/src/hooks/useTimerange.ts delete mode 100644 statviz/src/views/Statistics/DemographicView.tsx diff --git a/statviz/src/App.tsx b/statviz/src/App.tsx index 00b905a1a..b419af475 100644 --- a/statviz/src/App.tsx +++ b/statviz/src/App.tsx @@ -3,6 +3,7 @@ import DemographicView from "./views/Statistics/DemographicView"; import Dashboard from "./views/Dashboard/Dashboard"; import CreatedBoxes from "./components/visualizations/CreatedBoxes"; import TopProducts from "./components/visualizations/TopProducts"; +import DemographicChart from "./components/visualizations/DemographicChart"; function App() { return ( @@ -19,7 +20,7 @@ function App() { path="product-rank" element={} /> - } /> + } /> diff --git a/statviz/src/components/VisHeader.tsx b/statviz/src/components/VisHeader.tsx index 1b23dc549..60e4aaa71 100644 --- a/statviz/src/components/VisHeader.tsx +++ b/statviz/src/components/VisHeader.tsx @@ -22,6 +22,8 @@ import { CheckboxGroup, HStack, Wrap, + useCheckboxGroup, + Text, } from "@chakra-ui/react"; import { DownloadIcon } from "@chakra-ui/icons"; import domtoimage from "dom-to-image-more"; @@ -35,8 +37,8 @@ export default function VisHeader(params: { width: number, height: number, includeHeading: boolean, - includeTimestamp: boolean, - includeFromTo: boolean + includeTimerange: boolean, + includeTimestamp: boolean ) => void; onExportFinished: () => void; custom?: boolean; @@ -44,16 +46,13 @@ export default function VisHeader(params: { const [isLoading, setLoading] = useState(false); const [inputWidth, setInputWidth] = useState(800); const [inputHeight, setInputHeight] = useState(500); - const [includeHeading, setIncludeHeading] = useState(true); - const [includeTimestamp, setIncludeTimestamp] = useState(true); - const [includeFromTo, setIncludeFromTo] = useState(true); - const handleIncludeHeading = (e) => setIncludeHeading(e.target.checked); - const handleIncludeTimestamp = (e) => setIncludeTimestamp(e.target.checked); - const handleIncludeFromTo = (e) => setIncludeFromTo(e.target.checked); + const { value, getCheckboxProps } = useCheckboxGroup({ + defaultValue: ["heading", "timerange"], + }); const downloadImage = () => { - const chart = document.getElementById(params.visId); // params.visId + const chart = document.getElementById(params.visId)?.firstChild?.firstChild; domtoimage .toJpeg(chart, { @@ -76,10 +75,9 @@ export default function VisHeader(params: { }; const downloadImageSVG = () => { - const svgData = params.custom - ? document.getElementById(params.visId).outerHTML - : document.getElementById(params.visId)?.firstChild?.firstChild - ?.firstChild.outerHTML; + const svgData = document.getElementById(params.visId)?.firstChild + ?.firstChild.innerHTML; + const svgBlob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8", }); @@ -90,20 +88,23 @@ export default function VisHeader(params: { document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); + setLoading(false); + params.onExportFinished(); }; - const download = () => { + const download = (event) => { params.onExport( inputWidth, inputHeight, - includeHeading, - includeTimestamp, - includeFromTo + value.indexOf("heading") !== -1, + value.indexOf("timerange") !== -1, + value.indexOf("timestamp") !== -1 ); setLoading(true); // timeout triggers the rerender with loading animations before generating the image. // without the timeout the loading animation sometimes won't be triggered - setTimeout(downloadImage, 250); + if (event.target.value === "svg") setTimeout(downloadImageSVG, 500); + if (event.target.value === "jpg") setTimeout(downloadImage, 500); }; const getMaxWidth = () => { @@ -167,29 +168,20 @@ export default function VisHeader(params: {
- + Options Heading - - timerange + + Time Range - + Timestamp @@ -204,6 +196,7 @@ export default function VisHeader(params: {