@foreverrbum/ethsign
Version:
This package will allow you to electronically sign documents within your application
735 lines (635 loc) • 30.6 kB
JavaScript
/**
* @author pzhu, jbettenc
*/
import React,{ useState, useEffect } from "react";
import { Chart } from '../charts';
import { loadAllContractHistoryData,
network,
loadAllAddressCount,
loadDocumentsSignedByNetwork,
loadSignatureSignedByNetwork,
loadAllSignatureSignedData,
loadAllDocumentsSignedData
} from '../../helpers/graphql';
import moment from "moment";
import { CSVLink } from 'react-csv';
import _ from 'lodash';
import { parseContractHistoryData } from "./utils";
const statisticalData = {
'totalDocumentsSigned': 'Documents Signed',
'totalSignaturesSigned': 'Signatures Signed'
};
const CHART_COLOR = {
'totalDocumentsSigned': '#7cb5ec',
'totalSignaturesSigned': "#434348"
}
const TOTAL_DOCUMENTS_SIGNED = 'totalDocumentsSigned';
const TOTAL_SIGNATURES_SIGNED = 'totalSignaturesSigned';
export const DataDashboard = () => {
const [historyData, handleHistoryData] = useState();
const [allSignaturesSignedData, handleAllSignaturesSignedData] = useState();
const [allDocumentSignedData, handleAllDocumentSignedData] = useState();
const [allOptions, handleAllOptions] = useState();
const [timeOptions, handleTimeOptions] = useState();
const [aggregateOptions, handleAggregateOptions] = useState();
const [addressData, handleAddressData] = useState();
const [addressTotalOptions, handleAddressTotalOptions] = useState();
const [addressTimeOptions, handleAddressTimeOptions] = useState();
const [addressAggregateOptions, handleAddressAggregateOptions] = useState();
const [loading, handleLoading] = useState(false);
const [error, handleError] = useState(false);
const [currentLoadedStep, handleCurrentLoadedStep] = useState(0);
const [csvData, handleCSVData] = useState(null);
const [documentsSignedByNetwork, handleDocumentsSignedByNetwork] = useState(null);
const [signaturesSignedByNetwork, handleSignaturesSignedByNetwork] = useState(null);
useEffect(()=>{
const documentSignedEle = document.getElementById("documents_sign_num");
documentSignedEle?.setAttribute("style",'cursor:pointer');
documentSignedEle?.addEventListener('click', documentSignedListener);
const signatureSignedEle = document.getElementById("signature_sign_num");
signatureSignedEle?.setAttribute("style",'cursor:pointer');
signatureSignedEle?.addEventListener('click', signatureSignedListener);
return () => {
document.getElementById("documents_sign_num")?.removeEventListener('click', documentSignedListener);
document.getElementById("signature_sign_num")?.removeEventListener('click', signatureSignedListener);
}
},[historyData, addressData, allDocumentSignedData, allSignaturesSignedData, allOptions]);
const documentSignedListener = () => {
const contractsData = allDocumentSignedData?.reduce((acc, {data})=>{
if (data?.contracts) {
return acc.concat(data?.contracts)
}
return acc;
},[]);
const timeData = [];
contractsData?.sort((a, b) => a?.consensusTimestamp - b?.consensusTimestamp)
?.forEach((contract) => {
const { consensusTimestamp } = contract;
// parse every day timestamp
handleTimestamp(consensusTimestamp, timeData);
});
const len = contractsData?.length;
const timeTitle = `Documents Signed Count (${len})`;
const aggregateTile = `Documents Signed Count (${len}) - Aggregated`;
const { timeOpt, aggregateOpt } = generateOptions('Documents Signed', timeData, timeTitle, aggregateTile, CHART_COLOR[TOTAL_DOCUMENTS_SIGNED]);
handleTimeOptions(timeOpt);
handleAggregateOptions(aggregateOpt);
}
const signatureSignedListener = () => {
const contractsData = allSignaturesSignedData?.reduce((acc, {data: {events}})=>{
return acc.concat(events);
},[]);
const timeData = [];
contractsData?.sort((a, b) => a?.timestamp - b?.timestamp)
?.forEach((contract) => {
const { timestamp } = contract;
handleTimestamp(timestamp, timeData);
});
const len = contractsData?.length;
const timeTitle = `Signatures Signed Count (${len})`;
const aggregateTile = `Signatures Signed Count (${len}) - Aggregated`;
const { timeOpt, aggregateOpt } = generateOptions('Signatures Signed', timeData, timeTitle, aggregateTile, CHART_COLOR[TOTAL_SIGNATURES_SIGNED]);
handleTimeOptions(timeOpt);
handleAggregateOptions(aggregateOpt);
}
useEffect(()=> {
(async () => {
try {
handleError(false);
handleLoading(true);
// load contract data
handleCurrentLoadedStep(0);
const historyData = await loadAllContractHistoryData();
handleCurrentLoadedStep(1);
const {documentsSignedNumOnEachChain, signaturesSignedNumOnEachChain} = parseContractHistoryData(historyData);
handleHistoryData(historyData);
//load Address Count data
handleAddressData(await loadAllAddressCount());
handleCurrentLoadedStep(2);
// load document and signature count data
const dsbn = [];
const ssbn = [];
for(let idx in historyData) {
dsbn.push(handleDocumentSignedData(await loadDocumentsSignedByNetwork(network[idx])));
ssbn.push(handleSignatureSignedData(await loadSignatureSignedByNetwork(network[idx])));
}
handleCurrentLoadedStep(3);
handleDocumentsSignedByNetwork(dsbn);
handleSignaturesSignedByNetwork(ssbn);
// load all signature signed data
handleAllSignaturesSignedData(await loadAllSignatureSignedData(signaturesSignedNumOnEachChain));
handleCurrentLoadedStep(4);
// load all documents signed data
handleAllDocumentSignedData(await loadAllDocumentsSignedData(documentsSignedNumOnEachChain));
handleCurrentLoadedStep(5);
// Load default UI statistics from ethereum
const { timeOpt, aggregateOpt } = dsbn[0];
handleTimeOptions(timeOpt);
handleAggregateOptions(aggregateOpt);
handleLoading(false);
} catch(err) {
console.log(err);
handleLoading(false);
handleError(true);
}
})();
},[]);
useEffect(() => {
if(!historyData || !addressData || !allSignaturesSignedData || !allDocumentSignedData) {
return;
}
// History data portion
{
let allDocumentsSigned = 0;
let allSignaturesSigned = 0;
historyData?.forEach(({data:{infos}}) =>{
const {totalDocumentsSigned, totalSignaturesSigned} = infos[0];
allDocumentsSigned = allDocumentsSigned + totalDocumentsSigned;
allSignaturesSigned = allSignaturesSigned + totalSignaturesSigned;
})
const tmpOptions = {
chart: {
type: 'column'
},
title: {
text: "Contract Data",
},
subtitle: {
text: `<span id ="documents_sign_num">Documents Signed (${allDocumentsSigned}) </span><br/> <span id ="signature_sign_num">Signatures Signed (${allSignaturesSigned}) </span>`,
align: "center",
useHTML: true
},
xAxis: {
categories: network
},
yAxis: {
title: false
},
credits: {
enabled: false
},
series: []
};
const series = Object.keys(statisticalData).map(key => {
return {
cursor: 'pointer',
point: {
events: {
click: function () {
handleChartEvent(this.category, this.series.name);
}
}
},
name: statisticalData[key],
data: []
}
});
historyData?.forEach(d => {
const {data: {infos}} = d;
infos.forEach(info => {
Object.keys(statisticalData).forEach(key => {
const {data} = series.find(serie => serie.name === statisticalData[key]);
data.push(info[key]);
})
})
});
tmpOptions.series = series;
handleAllOptions(tmpOptions);
}
// Address Data portion
{
let addressNum = 0
addressData?.forEach((item)=>{
const {data}= item;
addressNum = (data?.users.length || 0 )+ addressNum
})
const tmpOptionsAddress = {
chart: {
type: 'column'
},
title: {
text: `Address Count (${addressNum})`,
},
xAxis: {
categories: network
},
yAxis: {
title: false
},
credits: {
enabled: false
},
series: []
};
const {series} = tmpOptionsAddress;
const seriesItem = {
cursor: 'pointer',
point: {
events: {
click: function () {
handleTotalAddressChartEvent(this.category);
}
}
},
name:"Address",
data: []
};
addressData?.forEach(address => {
const {networkName, data} = address;
//special
const len = data?.users?.length || 0;
seriesItem?.data.push(len);
});
series.push(seriesItem);
handleAddressTotalOptions(tmpOptionsAddress);
if (addressData) {
handleTotalAddressChartEvent(network[0]);
}
}
// Update our CSV data variable with the above data
if(historyData && addressData) {
handleCSVData(getCSVData());
}
}, [historyData, addressData, allSignaturesSignedData, allDocumentSignedData]);
const handleTotalAddressChartEvent = (networkName) => {
const res = addressData?.find((item) => item?.networkName === networkName);
const { data: {users}} = res;
const timeData = [];
users?.sort((a, b) => a?.joinedTimestamp - b?.joinedTimestamp)
?.forEach((user)=>{
const { joinedTimestamp } = user;
handleTimestamp(joinedTimestamp, timeData);
});
const timeTitle = `Address Count (${networkName})`;
const aggragatetTile = `Address Count - Aggregated (${networkName})`;
const { timeOpt, aggregateOpt } = generateOptions(networkName, timeData, timeTitle, aggragatetTile);
handleAddressTimeOptions(timeOpt);
handleAddressAggregateOptions(aggregateOpt);
}
function completionData (timeData) {
if (timeData?.length === 0) return [];
const endTime = moment();
const {year, month, date} = timeData[0];
let nextDay = moment([year, month - 1, date]);
const dataList = [];
while(isValidDate(endTime, nextDay)){
const tmpT = nextDay;
const tDate = tmpT.format("DD/MM/YYYY");
const item = timeData.find(({year, month, date}) => moment([year, month - 1, date]).format("DD/MM/YYYY") == tDate);
if (item) {
dataList.push(item);
} else {
dataList.push({
time: `${nextDay.year()}-${nextDay.month() + 1}-${nextDay.date()}`,
year: nextDay.year(),
month: nextDay.month() + 1,
date: nextDay.date(),
count:0
})
}
nextDay = nextDay.add(1, 'days');
}
return dataList;
}
function isValidDate (endTime, nextDay) {
return nextDay.isBefore(endTime);
}
const generateOptions = (seriesTitle, timeData, timeTitle, aggregateTile, color) => {
const dataList = completionData(timeData);
const aggregateTimeData = handleAggregateData(dataList);
const aggregateData = generateChartData(aggregateTimeData);
const numData = generateChartData(dataList);
const timeOpt = {
chart: {
type: 'column'
},
title: {
text:timeTitle,
},
xAxis: {
type:"datetime",
},
yAxis: {
title: false
},
credits: {
enabled: false
},
series: [{
title: `${seriesTitle}`,
name: `${seriesTitle}`,
data: numData,
color,
}]
}
const options = {
xAxis: {
type: 'datetime',
},
yAxis: {
min: 0,
title: false
},
credits: {
enabled: false
},
plotOptions: {
series: {
events: {
legendItemClick: function () {
return false;
}
},
marker: {
enabled: false
}
}
},
}
const aggregateOpTime = {
title: {
text: aggregateTile,
},
};
const aggregateOpSeries = {series: [
{
title: `${seriesTitle}`,
name: `${seriesTitle}`,
data: aggregateData,
color
}
]};
const aggregateOpt = {...options, ...aggregateOpTime, ...aggregateOpSeries}
return { timeOpt, aggregateOpt };
}
const handleTimestamp = (timestamp, timeData) => {
const {year, month, date, time} = parseTimestamp(timestamp);
const result = timeData.find(item => item?.time === time);
if (result) {
result.count = result?.count + 1;
} else {
timeData.push({
time,
year,
month,
date,
count: 1
})
}
}
const parseTimestamp = (timestamp) => {
const utcTime = moment.unix(timestamp).utc();
const year = utcTime.year();
const month = utcTime.month() + 1;
const date = utcTime.date();
const time = `${year}-${month}-${date}`;
return {year, month, date, time};
}
const generateChartData = (timeData) => {
return sortTimeData(timeData)?.map(({year, month, date, count})=> {
return [Date.UTC(year, month-1, date), count]
});
}
const sortTimeData = (timeData) => {
return timeData.sort((a,b)=>{
const aTime = moment([a.year, a.month, a.date]).unix();
const bTime = moment([b.year, b.month, b.date]).unix();
return aTime - bTime;
});
}
const handleAggregateData = (timeData) => {
const tmp = sortTimeData(_.cloneDeep(timeData));
return tmp?.map((item, index) => {
let count;
if (index !== 0) {
count = item.count + tmp[index -1].count;
item.count = count;
}
return item;
})
}
const handleDocumentSignedData = (documentSignedData) => {
const {networkName, data } = documentSignedData;
const timeData = [];
data?.contracts?.filter(({consensusTimestamp}) => consensusTimestamp !== "0")
?.sort((a, b) => a?.consensusTimestamp - b?.consensusTimestamp)
?.forEach((contract)=>{
const { consensusTimestamp } = contract;
// parse every day timestamp
handleTimestamp(consensusTimestamp, timeData);
});
const timeTitle = `Documents Signed Count (${networkName})`;
const aggragatetTile = `Documents Signed Count - Aggregated (${networkName})`;
return generateOptions(networkName, timeData, timeTitle, aggragatetTile, CHART_COLOR[TOTAL_DOCUMENTS_SIGNED]);
}
const handleSignatureSignedData = (signatureSignedData) => {
const {networkName, data:{events}} = signatureSignedData;
const timeData = [];
events?.sort((a,b) => a?.timestamp - b?.timestamp)
?.forEach(({timestamp}) => {
handleTimestamp(timestamp, timeData);
});
const timeTitle = `Signature Signed Count (${networkName})`;
const aggregateTile = `Signature Signed Count - Aggregated (${networkName})`;
return generateOptions(networkName, timeData, timeTitle, aggregateTile, CHART_COLOR[TOTAL_SIGNATURES_SIGNED]);
}
const handleChartEvent = async (networkName, type) => {
const result = Object.keys(statisticalData).find(key => statisticalData[key] === type);
switch (result) {
case TOTAL_DOCUMENTS_SIGNED:
const { timeOpt, aggregateOpt } = handleDocumentSignedData(await loadDocumentsSignedByNetwork(networkName));
handleTimeOptions(timeOpt);
handleAggregateOptions(aggregateOpt);
break;
case TOTAL_SIGNATURES_SIGNED:
const { timeOpt:timeOp, aggregateOpt:aggregateOp } = handleSignatureSignedData(await loadSignatureSignedByNetwork(networkName));
handleTimeOptions(timeOp);
handleAggregateOptions(aggregateOp);
break;
}
}
const headers = [
{ label: "Network", key: "network" },
{ label: "Documents Signed", key: "docsignedlength" },
{ label: "Signatures Signed", key: "sigsignedlength" },
{ label: "Address Count", key: "addresscountog" },
{ label: "", key: "blank" },
{ label: "Date", key: "date" },
{ label: "Signed Document Count (ethereum)", key: "signeddoccount.ethereum" },
{ label: "Signed Document Count (ropsten)", key: "signeddoccount.ropsten" },
{ label: "Signed Document Count (bsc)", key: "signeddoccount.bsc" },
{ label: "Signed Document Count (avalanche)", key: "signeddoccount.avalanche" },
{ label: "Signed Document Count (polygon)", key: "signeddoccount.polygon" },
{ label: "Signed Document Count (fantom)", key: "signeddoccount.fantom" },
{ label: "", key: "blank" },
{ label: "Aggregate Signed Document Count (ethereum)", key: "signeddoccountagg.ethereum" },
{ label: "Aggregate Signed Document Count (ropsten)", key: "signeddoccountagg.ropsten" },
{ label: "Aggregate Signed Document Count (bsc)", key: "signeddoccountagg.bsc" },
{ label: "Aggregate Signed Document Count (avalanche)", key: "signeddoccountagg.avalanche" },
{ label: "Aggregate Signed Document Count (polygon)", key: "signeddoccountagg.polygon" },
{ label: "Aggregate Signed Document Count (fantom)", key: "signeddoccountagg.fantom" },
{ label: "", key: "blank" },
{ label: "Signature Count (ethereum)", key: "signaturecount.ethereum" },
{ label: "Signature Count (ropsten)", key: "signaturecount.ropsten" },
{ label: "Signature Count (bsc)", key: "signaturecount.bsc" },
{ label: "Signature Count (avalanche)", key: "signaturecount.avalanche" },
{ label: "Signature Count (polygon)", key: "signaturecount.polygon" },
{ label: "Signature Count (fantom)", key: "signaturecount.fantom" },
{ label: "", key: "blank" },
{ label: "Aggregate Signature Count (ethereum)", key: "signaturecountagg.ethereum" },
{ label: "Aggregate Signature Count (ropsten)", key: "signaturecountagg.ropsten" },
{ label: "Aggregate Signature Count (bsc)", key: "signaturecountagg.bsc" },
{ label: "Aggregate Signature Count (avalanche)", key: "signaturecountagg.avalanche" },
{ label: "Aggregate Signature Count (polygon)", key: "signaturecountagg.polygon" },
{ label: "Aggregate Signature Count (fantom)", key: "signaturecountagg.fantom" },
{ label: "", key: "blank" },
{ label: "Address Count (ethereum)", key: "addresscount.ethereum" },
{ label: "Address Count (ropsten)", key: "addresscount.ropsten" },
{ label: "Address Count (bsc)", key: "addresscount.bsc" },
{ label: "Address Count (avalanche)", key: "addresscount.avalanche" },
{ label: "Address Count (polygon)", key: "addresscount.polygon" },
{ label: "Address Count (fantom)", key: "addresscount.fantom" },
{ label: "", key: "blank" },
{ label: "Aggregate Address Count (ethereum)", key: "addresscountagg.ethereum" },
{ label: "Aggregate Address Count (ropsten)", key: "addresscountagg.ropsten" },
{ label: "Aggregate Address Count (bsc)", key: "addresscountagg.bsc" },
{ label: "Aggregate Address Count (avalanche)", key: "addresscountagg.avalanche" },
{ label: "Aggregate Address Count (polygon)", key: "addresscountagg.polygon" },
{ label: "Aggregate Address Count (fantom)", key: "addresscountagg.fantom" },
];
const getCSVData = () => {
// Signature counts + Signed document counts
const numDataSig = [], aggregateDataSig = [], numDataDoc = [], aggregateDataDoc = [];
for(let idx in signaturesSignedByNetwork) {
const { timeOpt, aggregateOpt } = signaturesSignedByNetwork[idx];
numDataSig.push(timeOpt.series[0].data);
aggregateDataSig.push(aggregateOpt.series[0].data);
const { timeOpt: timeOptDoc, aggregateOpt: aggregateOptDoc } = documentsSignedByNetwork[idx];
numDataDoc.push(timeOptDoc.series[0].data);
aggregateDataDoc.push(aggregateOptDoc.series[0].data);
}
// Address Count
const csvAddressData = [];
for(let idx in addressData) {
const { data: {users} } = addressData[idx];
const timeDataAddress = [];
users?.sort((a, b) => a?.joinedTimestamp - b?.joinedTimestamp)
?.forEach((user)=>{
const { joinedTimestamp } = user;
handleTimestamp(joinedTimestamp, timeDataAddress);
});
const dataListAddress = completionData(timeDataAddress);
const aggregateTimeDataAddress = handleAggregateData(dataListAddress);
const aggregateDataAddress = generateChartData(aggregateTimeDataAddress);
const numDataAddress = generateChartData(dataListAddress);
csvAddressData.push({
aggregateDataAddress: aggregateDataAddress,
numDataAddress: numDataAddress,
});
}
let contractData = [];
// Document and signature counts
for(let net in numDataSig[0]) {
contractData.push({
network: historyData[net] ? historyData[net].networkName : "",
docsignedlength: historyData[net]?.data ? historyData[net].data.infos[0].totalDocumentsSigned : "",
sigsignedlength: historyData[net]?.data ? historyData[net].data.infos[0].totalSignaturesSigned : "",
blank: "",
date: new Date(numDataSig[0][net][0]).toDateString(),
signeddoccount: {
ethereum: numDataDoc[0][net] ? numDataDoc[0][net][1] : "0",
ropsten: numDataDoc[1][net] ? numDataDoc[1][net][1] : "0",
bsc: numDataDoc[2][net] ? numDataDoc[2][net][1] : "0",
avalanche: numDataDoc[3][net] ? numDataDoc[3][net][1] : "0",
polygon: numDataDoc[4][net] ? numDataDoc[4][net][1] : "0",
fantom: numDataDoc[0][net] ? numDataDoc[0][net][1] : "0",
},
signeddoccountagg: {
ethereum: aggregateDataDoc[0][net] ? aggregateDataDoc[0][net][1] : "0",
ropsten: aggregateDataDoc[1][net] ? aggregateDataDoc[1][net][1] : "0",
bsc: aggregateDataDoc[2][net] ? aggregateDataDoc[2][net][1] : "0",
avalanche: aggregateDataDoc[3][net] ? aggregateDataDoc[3][net][1] : "0",
polygon: aggregateDataDoc[4][net] ? aggregateDataDoc[4][net][1] : "0",
fantom: aggregateDataDoc[0][net] ? aggregateDataDoc[0][net][1] : "0",
},
signaturecount: {
ethereum: numDataSig[0][net] ? numDataSig[0][net][1] : "0",
ropsten: numDataSig[1][net] ? numDataSig[1][net][1] : "0",
bsc: numDataSig[2][net] ? numDataSig[2][net][1] : "0",
avalanche: numDataSig[3][net] ? numDataSig[3][net][1] : "0",
polygon: numDataSig[4][net] ? numDataSig[4][net][1] : "0",
fantom: numDataSig[0][net] ? numDataSig[0][net][1] : "0",
},
signaturecountagg: {
ethereum: aggregateDataSig[0][net] ? aggregateDataSig[0][net][1] : "0",
ropsten: aggregateDataSig[1][net] ? aggregateDataSig[1][net][1] : "0",
bsc: aggregateDataSig[2][net] ? aggregateDataSig[2][net][1] : "0",
avalanche: aggregateDataSig[3][net] ? aggregateDataSig[3][net][1] : "0",
polygon: aggregateDataSig[4][net] ? aggregateDataSig[4][net][1] : "0",
fantom: aggregateDataSig[0][net] ? aggregateDataSig[0][net][1] : "0",
},
addresscount: {
ethereum: csvAddressData[0].numDataAddress[net] ? csvAddressData[0].numDataAddress[net][1] : "0",
ropsten: csvAddressData[1].numDataAddress[net] ? csvAddressData[1].numDataAddress[net][1] : "0",
bsc: csvAddressData[2].numDataAddress[net] ? csvAddressData[2].numDataAddress[net][1] : "0",
avalanche: csvAddressData[3].numDataAddress[net] ? csvAddressData[3].numDataAddress[net][1] : "0",
polygon: csvAddressData[4].numDataAddress[net] ? csvAddressData[4].numDataAddress[net][1] : "0",
fantom: csvAddressData[5].numDataAddress[net] ? csvAddressData[5].numDataAddress[net][1] : "0",
},
addresscountagg: {
ethereum: csvAddressData[0].aggregateDataAddress[net] ? csvAddressData[0].aggregateDataAddress[net][1] : "0",
ropsten: csvAddressData[1].aggregateDataAddress[net] ? csvAddressData[1].aggregateDataAddress[net][1] : "0",
bsc: csvAddressData[2].aggregateDataAddress[net] ? csvAddressData[2].aggregateDataAddress[net][1] : "0",
avalanche: csvAddressData[3].aggregateDataAddress[net] ? csvAddressData[3].aggregateDataAddress[net][1] : "0",
polygon: csvAddressData[4].aggregateDataAddress[net] ? csvAddressData[4].aggregateDataAddress[net][1] : "0",
fantom: csvAddressData[5].aggregateDataAddress[net] ? csvAddressData[5].aggregateDataAddress[net][1] : "0",
}
});
}
// Add address counts
for(let net in addressData) {
contractData[net].addresscountog = addressData[net].data.users.length;
}
let data = contractData;
return data;
}
if(error) {
return (
<div className="m-auto text-lg text-gray-60">
Error loading data. Check console for a detailed error message.
</div>
);
}
if(loading) {
return (
<div className="m-auto text-lg text-gray-60">
Loading... ({currentLoadedStep}/5)
</div>
);
}
return (
<div className="flex justify-center flex-wrap mt-16 px-2 sm:px-16 mb-auto max-w-7xl">
<CSVLink className="cursor-pointer text-center ml-auto mt-3 sm:mt-0 sm:w-32 font-bold flex-grow-0 bg-orange-500 focus:outline-none text-gray-40 py-2 rounded-sm hover:bg-orange-600" onClick={(event) => {
// Returning false will not export the svg. Since we are still loading data, we don't want to give an incomplete csv file.
if(loading) {
return false;
}
}}
headers={headers}
data={csvData ? csvData : ""}
filename={`data-export-${(new Date().toISOString().replace(':', '-').substring(0, 16))}Z.csv`}>
{loading ? 'Loading...' : 'Export to CSV'}
</CSVLink>
<div className="flex justify-around flex-wrap w-full">
<Chart options={allOptions} className="my-4 w-full sm:w-1/3 p-4"/>
<Chart options={timeOptions} className="my-4 w-full sm:w-1/3 p-4"/>
<Chart options={aggregateOptions} className="my-4 w-full sm:w-1/3 p-4"/>
</div>
<div className="flex justify-around flex-wrap w-full">
<Chart options={addressTotalOptions} className="my-4 w-full sm:w-1/3 p-4"/>
<Chart options={addressTimeOptions} className="my-4 w-full sm:w-1/3 p-4"/>
<Chart options={addressAggregateOptions} className="my-4 w-full sm:w-1/3 p-4"/>
</div>
</div>
)
}