parent
9a41f72e70
commit
9e7e2c690f
@ -0,0 +1,125 @@
|
||||
import React from 'react';
|
||||
// import dynamic from 'next/dynamic'
|
||||
// import {buildURI} from 'react-csv';
|
||||
// /**
|
||||
// * See https://github.com/react-csv/react-csv/issues/87
|
||||
// */
|
||||
export const isSafari = () => /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
|
||||
export const isJsons = ((array) => Array.isArray(array) && array.every(
|
||||
row => (typeof row === 'object' && !(row instanceof Array))
|
||||
));
|
||||
|
||||
export const isArrays = ((array) => Array.isArray(array) && array.every(
|
||||
row => Array.isArray(row)
|
||||
));
|
||||
|
||||
export const jsonsHeaders = ((array) => Array.from(
|
||||
array.map(json => Object.keys(json))
|
||||
.reduce((a, b) => new Set([...a, ...b]), [])
|
||||
));
|
||||
|
||||
export const jsons2arrays = (jsons, headers) => {
|
||||
headers = headers || jsonsHeaders(jsons);
|
||||
|
||||
// allow headers to have custom labels, defaulting to having the header data key be the label
|
||||
let headerLabels = headers;
|
||||
let headerKeys = headers;
|
||||
if (isJsons(headers)) {
|
||||
headerLabels = headers.map((header) => header.label);
|
||||
headerKeys = headers.map((header) => header.key);
|
||||
}
|
||||
|
||||
const data = jsons.map((object) => headerKeys.map((header) => getHeaderValue(header, object)));
|
||||
return [headerLabels, ...data];
|
||||
};
|
||||
|
||||
export const getHeaderValue = (property, obj) => {
|
||||
const foundValue = property
|
||||
.replace(/\[([^\]]+)]/g, ".$1")
|
||||
.split(".")
|
||||
.reduce(function (o, p, i, arr) {
|
||||
// if at any point the nested keys passed do not exist, splice the array so it doesnt keep reducing
|
||||
const value = o[p];
|
||||
if (value === undefined || value === null) {
|
||||
arr.splice(1);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}, obj);
|
||||
// if at any point the nested keys passed do not exist then looks for key `property` in object obj
|
||||
return (foundValue === undefined) ? ((property in obj) ? obj[property] : '') : foundValue;
|
||||
}
|
||||
|
||||
export const elementOrEmpty = (element) =>
|
||||
(typeof element === 'undefined' || element === null) ? '' : element;
|
||||
|
||||
export const joiner = ((data, separator = ',', enclosingCharacter = '"') => {
|
||||
return data
|
||||
.filter(e => e)
|
||||
.map(
|
||||
row => row
|
||||
.map((element) => elementOrEmpty(element))
|
||||
.map(column => `${enclosingCharacter}${column}${enclosingCharacter}`)
|
||||
.join(separator)
|
||||
)
|
||||
.join(`\n`);
|
||||
});
|
||||
|
||||
export const arrays2csv = ((data, headers, separator, enclosingCharacter) =>
|
||||
joiner(headers ? [headers, ...data] : data, separator, enclosingCharacter)
|
||||
);
|
||||
|
||||
export const jsons2csv = ((data, headers, separator, enclosingCharacter) =>
|
||||
joiner(jsons2arrays(data, headers), separator, enclosingCharacter)
|
||||
);
|
||||
|
||||
export const string2csv = ((data, headers, separator) =>
|
||||
(headers) ? `${headers.join(separator)}\n${data}` : data.replace(/"/g, '""')
|
||||
);
|
||||
|
||||
export const toCSV = (data, headers, separator, enclosingCharacter) => {
|
||||
if (isJsons(data)) return jsons2csv(data, headers, separator, enclosingCharacter);
|
||||
if (isArrays(data)) return arrays2csv(data, headers, separator, enclosingCharacter);
|
||||
if (typeof data === 'string') return string2csv(data, headers, separator);
|
||||
throw new TypeError(`Data should be a "String", "Array of arrays" OR "Array of objects" `);
|
||||
};
|
||||
|
||||
|
||||
const CSVLink = ({filename, data, children}) => {
|
||||
|
||||
console.log("DATA", data);
|
||||
const buildURI = ((data, uFEFF, headers, separator, enclosingCharacter) => {
|
||||
console.log("DATA2", data);
|
||||
const csv = toCSV(data, headers, separator, enclosingCharacter);
|
||||
console.log("CSV", csv);
|
||||
const type = isSafari() ? 'application/csv' : 'text/csv';
|
||||
const blob = new Blob([uFEFF ? '\uFEFF' : '', csv], {type});
|
||||
const dataURI = `data:${type};charset=utf-8,${uFEFF ? '\uFEFF' : ''}${csv}`;
|
||||
|
||||
const URL = window.URL || window.webkitURL;
|
||||
|
||||
return (typeof URL.createObjectURL === 'undefined')
|
||||
? dataURI
|
||||
: URL.createObjectURL(blob);
|
||||
});
|
||||
|
||||
const isNodeEnvironment = typeof window === 'undefined';
|
||||
const uFEFF = true;
|
||||
const headers = undefined;
|
||||
const separator = ",";
|
||||
const enclosingCharacter = '"';
|
||||
const href = isNodeEnvironment ? '' : buildURI(data, uFEFF, headers, separator, enclosingCharacter)
|
||||
|
||||
return (
|
||||
<a
|
||||
download={filename}
|
||||
target="_blank"
|
||||
href={href}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export default CSVLink;
|
@ -0,0 +1,206 @@
|
||||
import {useEffect, useLayoutEffect, useRef, useState} from 'react';
|
||||
import {GradeResultInterface, MeritProfileInterface} from '@services/type';
|
||||
import {getMajorityGrade} from '@services/majorityJudgment';
|
||||
|
||||
|
||||
interface ParamsInterface {
|
||||
numVotes: number;
|
||||
outgaugeThreshold: number;
|
||||
}
|
||||
|
||||
interface GradeBarInterface {
|
||||
grade: GradeResultInterface;
|
||||
size: number;
|
||||
index: number;
|
||||
params: ParamsInterface;
|
||||
}
|
||||
|
||||
const GradeBar = ({index, grade, size, params}: GradeBarInterface) => {
|
||||
|
||||
const width = `${size * 100}%`
|
||||
const textWidth = Math.floor(100 * size)
|
||||
|
||||
const left = `${(size * 100) / 2}%`;
|
||||
const top = index % 2 ? "20px" : "-20px";
|
||||
|
||||
if (size < 0.001) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-100"
|
||||
style={{flexBasis: width, backgroundColor: grade.color, minHeight: "20px"}}>
|
||||
{
|
||||
/* size < params.outgaugeThreshold ? (
|
||||
<span
|
||||
style={{
|
||||
left: left,
|
||||
top: top,
|
||||
display: "relative",
|
||||
backgroundColor: grade.color,
|
||||
}}
|
||||
>
|
||||
{textWidth}%
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
{Math.floor(100 * size)}%
|
||||
</span>
|
||||
)
|
||||
*/ }
|
||||
</div>)
|
||||
}
|
||||
|
||||
|
||||
const DashedMedian = () => {
|
||||
|
||||
return <div className="position-relative d-flex justify-content-center"
|
||||
style={{top: "60px", height: "50px"}}
|
||||
>
|
||||
<div
|
||||
className="border h-100 border-1 border-dark border-opacity-75 border-dashed"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
const MajorityGrade = ({grade, left}) => {
|
||||
const spanRef = useRef<HTMLDivElement>();
|
||||
|
||||
const [width, setWidth] = useState(0)
|
||||
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (spanRef && spanRef.current) {
|
||||
setWidth(spanRef.current.offsetWidth);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (spanRef && spanRef.current) {
|
||||
setWidth(spanRef.current.offsetWidth);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
ref={spanRef}
|
||||
style={{
|
||||
color: 'white',
|
||||
backgroundColor: grade.color,
|
||||
left: `calc(${left * 100}% - ${width / 2}px)`,
|
||||
top: "-20px",
|
||||
}}
|
||||
className="p-2 position-relative fw-bold rounded-1 text-center"
|
||||
>
|
||||
{grade.name}
|
||||
|
||||
</span>
|
||||
<span style={{
|
||||
position: "relative",
|
||||
width: 0,
|
||||
height: 0,
|
||||
left: `calc(${left * 100}% - ${width + 6}px)`,
|
||||
top: "20px",
|
||||
borderLeftWidth: 6,
|
||||
borderRightWidth: 6,
|
||||
borderTopWidth: 12,
|
||||
borderStyle: 'solid',
|
||||
backgroundColor: 'transparent',
|
||||
borderLeftColor: 'transparent',
|
||||
borderRightColor: 'transparent',
|
||||
borderTopColor: grade.color,
|
||||
color: "transparent",
|
||||
}}></span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface MeritProfileBarInterface {
|
||||
profile: MeritProfileInterface;
|
||||
grades: Array<GradeResultInterface>;
|
||||
}
|
||||
|
||||
const MeritProfileBar = ({profile, grades}: MeritProfileBarInterface) => {
|
||||
|
||||
const gradesByValue: {[key: number]: GradeResultInterface} = {}
|
||||
grades.forEach(g => gradesByValue[g.value] = g)
|
||||
|
||||
const numVotes = Object.values(profile).reduce((a, b) => a + b, 0)
|
||||
const values = grades.map(g => g.value).sort();
|
||||
const normalized = values.map(value => value in profile ? profile[value] / numVotes : 0)
|
||||
// low values means great grade
|
||||
|
||||
// find the majority grade
|
||||
const majorityIdx = getMajorityGrade(profile)
|
||||
const majorityGrade = grades[majorityIdx]
|
||||
|
||||
const proponentSizes = normalized.filter((_, i) => values[i] < majorityGrade.value)
|
||||
const proponentWidth = proponentSizes.reduce((a, b) => a + b, 0)
|
||||
|
||||
const opponentSizes = normalized.filter((_, i) => values[i] > majorityGrade.value)
|
||||
const opponentWidth = opponentSizes.reduce((a, b) => a + b, 0)
|
||||
|
||||
// is proponent higher than opposant?
|
||||
const proponentMajority = proponentWidth > opponentWidth;
|
||||
|
||||
// for mobile phone, we outgauge earlier than on desktop
|
||||
const innerWidth = typeof window !== 'undefined' && window.innerWidth ? window.innerWidth : 1000;
|
||||
|
||||
const params: ParamsInterface = {
|
||||
outgaugeThreshold: (innerWidth <= 760) ? 0.05 : 0.03,
|
||||
numVotes,
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<DashedMedian />
|
||||
<MajorityGrade
|
||||
grade={majorityGrade}
|
||||
left={proponentWidth + normalized[majorityIdx] / 2}
|
||||
/>
|
||||
<div className='d-flex'>
|
||||
<div
|
||||
className={`d-flex border border-${proponentMajority ? 2 : 1} border-success`}
|
||||
style={{flexBasis: `${proponentWidth * 100}%`}}
|
||||
>
|
||||
{values.filter(v => v < majorityGrade.value).map(v => {
|
||||
const index = values.indexOf(v);
|
||||
const size = proponentWidth < 1e-3 ? 0 : normalized[index] / proponentWidth;
|
||||
return (
|
||||
<GradeBar index={index} params={params} grade={grades[v]} key={index} size={size} />
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="border border-2 border-primary"
|
||||
style={{flexBasis: `${normalized[majorityIdx] * 100}%`}}
|
||||
|
||||
>
|
||||
{values.filter(v => v === majorityGrade.value).map(v => {
|
||||
const index = values.indexOf(v);
|
||||
return (
|
||||
<GradeBar index={index} params={params} grade={grades[v]} key={index} size={1} />
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={`d-flex border border-${proponentMajority ? 1 : 2} border-danger`}
|
||||
style={{flexBasis: `${opponentWidth * 100}%`}}
|
||||
>
|
||||
{values.filter(v => v > majorityGrade.value).map(v => {
|
||||
const index = values.indexOf(v);
|
||||
const size = opponentWidth < 1e-3 ? 0 : normalized[index] / opponentWidth;
|
||||
return (
|
||||
<GradeBar index={index} params={params} grade={grades[v]} key={index} size={size} />
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{ /* <div className='median dash'> </div> */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default MeritProfileBar;
|
@ -0,0 +1,50 @@
|
||||
import Link from 'next/link'
|
||||
import {useTranslation} from 'next-i18next';
|
||||
import {serverSideTranslations} from 'next-i18next/serverSideTranslations';
|
||||
import {faArrowRight} from '@fortawesome/free-solid-svg-icons';
|
||||
import {Container} from 'reactstrap';
|
||||
import ErrorMessage from '@components/Error';
|
||||
import {RESULTS} from '@services/routes'
|
||||
import {displayRef} from '@services/utils'
|
||||
import Blur from '@components/Blur'
|
||||
import Button from '@components/Button';
|
||||
|
||||
|
||||
export const getServerSideProps = async ({query: {pid, tid}, locale}) => {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['resource'])),
|
||||
token: tid,
|
||||
electionRef: pid.replaceAll("-", "")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const End = ({electionRef, token}) => {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Blur />
|
||||
<div className="w-100 h-100 d-flex flex-column justify-content-center align-items-center">
|
||||
<ErrorMessage msg={t('error.ended-election')} />
|
||||
<Container className="full-height-container">
|
||||
<Link
|
||||
className="d-grid w-100 mt-5"
|
||||
href={`${RESULTS}/${displayRef(electionRef)}/${token ? token : ""}`}>
|
||||
<Button
|
||||
color="secondary"
|
||||
outline={true}
|
||||
type="submit"
|
||||
icon={faArrowRight}
|
||||
position="right"
|
||||
>
|
||||
{t('vote.go-to-results')}</Button>
|
||||
</Link>
|
||||
</Container>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default End;
|
@ -1,25 +0,0 @@
|
||||
import dynamic from 'next/dynamic';
|
||||
// import Plot from 'react-plotly.js';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
class BarChart extends Component {
|
||||
render() {
|
||||
return <div></div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default BarChart;
|
||||
|
||||
// <Plot
|
||||
// data={[
|
||||
// {
|
||||
// type: 'bar',
|
||||
// x: ['Taubira', 'Hidalgo', 'Mélenchon'],
|
||||
// y: [29, 150, 85]
|
||||
// }
|
||||
// ]}
|
||||
// layout={{width: 1000, height: 500, title: 'Nombre de voix par candidat'}}
|
||||
// config={{
|
||||
// displayModeBar: false // this is the line that hides the bar.
|
||||
// }}
|
||||
// />
|
Loading…
Reference in new issue