Skip to content

Commit

Permalink
Add PR logs
Browse files Browse the repository at this point in the history
  • Loading branch information
ArendPeter committed Dec 16, 2024
1 parent b76a7e7 commit d91d526
Show file tree
Hide file tree
Showing 5 changed files with 39 additions and 3 deletions.
23 changes: 21 additions & 2 deletions packages/backend/src/Tabulators/AllocatedScore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ballot, candidate, fiveStarCount, allocatedScoreResults, allocatedScore
import { IparsedData } from './ParseData'
const Fraction = require('fraction.js');
import { sortByTieBreakOrder } from "./Star";
import { commaListFormatter } from "./Util";

const ParseData = require("./ParseData");
declare namespace Intl {
Expand Down Expand Up @@ -50,18 +51,19 @@ export function AllocatedScore(candidates: string[], votes: ballot[], nWinners =
roundResults: [],
summaryData: summaryData,
tieBreakType: 'none',
logs: []
}
var remainingCandidates = [...summaryData.candidates]
// Run election rounds until there are no remaining candidates
// Keep running elections rounds even if all seats have been filled to determine candidate order


// Normalize scores array
var scoresNorm = normalizeArray(parsedData.scores, maxScore);

// Find number of voters and quota size
const V = scoresNorm.length;
const quota = new Fraction(V).div(nWinners);
results.logs.push(`${nWinners} winners will represent ${V} voters, so winners will need to represent a quota of ${rounded(quota)} voters each.`)
var num_candidates = candidates.length

var ballot_weights: typeof Fraction[] = Array(V).fill(new Fraction(1));
Expand All @@ -71,6 +73,7 @@ export function AllocatedScore(candidates: string[], votes: ballot[], nWinners =
var candidatesByRound: candidate[][] = []
// run loop until specified number of winners are found
while (results.elected.length < nWinners) {
results.logs.push(`Round ${results.elected.length+1} of tabulation...`)
// weight the scores
var weighted_scores: ballotFrac[] = Array(scoresNorm.length);
var weighted_sums: typeof Fraction[] = Array(num_candidates).fill(new Fraction(0));
Expand All @@ -91,9 +94,15 @@ export function AllocatedScore(candidates: string[], votes: ballot[], nWinners =
// get index of winner
var maxAndTies = indexOfMax(weighted_sums, summaryData.candidates, breakTiesRandomly);
var w = maxAndTies.maxIndex;
results.tied.push(maxAndTies.ties);
// add winner to winner list
results.logs.push(`${summaryData.candidates[w].name} is scored highest with ${rounded(weighted_sums[w].mul(maxScore))} stars and is elected!`)
results.elected.push(summaryData.candidates[w]);
// Update scores
// the and is not perfect, but I'll fix this once I switch to localizing
if(maxAndTies.ties.length > 1){
results.logs.push(`${maxAndTies.ties.map(c => c.name).join(' and ')} are tied for the highest scores!`)
}
results.tied.push(maxAndTies.ties);
// Set all scores for winner to zero
scoresNorm.forEach((ballot, b) => {
ballot[w] = new Fraction(0)
Expand Down Expand Up @@ -135,6 +144,7 @@ export function AllocatedScore(candidates: string[], votes: ballot[], nWinners =
summaryData.spentAboves.push(spent_above.valueOf());

if (spent_above.valueOf() > 0) {
results.logs.push(`The ${rounded(spent_above)} voters who gave ${summaryData.candidates[w].name} more than ${rounded(split_point.mul(maxScore))} stars are fully represented and will be weighted to 0 for future rounds.`)
cand_df.forEach((c, i) => {
if (c.weighted_score.compare(split_point) > 0) {
cand_df[i].ballot_weight = new Fraction(0);
Expand All @@ -144,6 +154,11 @@ export function AllocatedScore(candidates: string[], votes: ballot[], nWinners =

var weight_on_split = findWeightOnSplit(cand_df, split_point);

// quota = spent_above + weight_on_split*new_weight
let new_weight = (quota.sub(spent_above)).div(weight_on_split);
results.logs.push(`(${rounded(quota)} - ${rounded(spent_above)}) / ${rounded(weight_on_split)} = ${rounded(new_weight)}`)
results.logs.push(`The ${rounded(weight_on_split)} voters who gave ${summaryData.candidates[w].name} ${rounded(split_point.mul(maxScore))} stars are partially represented and will be reweighted by ${rounded(new_weight)} for future rounds.`)

summaryData.weight_on_splits.push(weight_on_split.valueOf());
ballot_weights = updateBallotWeights(
cand_df,
Expand All @@ -166,6 +181,10 @@ export function AllocatedScore(candidates: string[], votes: ballot[], nWinners =
return results
}

function rounded(n: typeof Fraction){
return Math.round(n*100)/100
}

function getSummaryData(candidates: string[], parsedData: IparsedData, randomTiebreakOrder: number[]): allocatedScoreSummaryData {
const nCandidates = candidates.length
if (randomTiebreakOrder.length < nCandidates) {
Expand Down
13 changes: 13 additions & 0 deletions packages/frontend/src/components/Election/Results/Results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import SupportBlurb from "../SupportBlurb";
import ColumnDistributionWidget from "./components/ColumnDistributionWidget";
import NameRecognitionWidget from "./components/NameRecognitionWidget";
import ScoreRangeWidget from "./components/ScoreRangeWidget";
import useFeatureFlags from "~/components/FeatureFlagContextProvider";

function STARResultsViewer({ filterRandomFromLogs }: {filterRandomFromLogs: boolean }) {
let i = 0;
Expand Down Expand Up @@ -336,6 +337,7 @@ function ResultsViewer({ methodKey, children }:{methodKey: string, children:any}
}

function PRResultsViewer() {
const flags = useFeatureFlags();
let {results, t, race} = useRace();
results = results as allocatedScoreResults;
const [page, setPage] = useState(1);
Expand Down Expand Up @@ -399,6 +401,17 @@ function PRResultsViewer() {
<Widget title={t('results.star_pr.table_title')}>
<ResultsTable className='starPRTable' data={tabulationRows}/>
</Widget>
{flags.isSet('ALL_STATS') &&
<Widget title={t('results.star.detailed_steps_title')}>
<div className='detailedSteps'>
<ol style={{textAlign: 'left'}}>
{results.logs.map((log, i) => (<li key={i}>
{typeof log === 'string' ? log : t(log['key'], log)}
</li>))}
</ol>
</div>
</Widget>
}
</WidgetContainer>
<DetailExpander level={1}>
<WidgetContainer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ const ViewElectionResults = () => {
{t('results.election_title', {title: election.title})}
</Typography>
{isPending && <div> {t('results.loading_election')} </div>}
{!isPending && !data && <>
The election admins have not released the results yet. Feel free to swing back later 😊.
</>}

{data?.results.map((results, race_index) => (
<Results
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/i18n/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ about:
# Results page
results:
preliminary_title: PRELIMINARY RESULTS
official_title: OFFICIAL_RESULTS
official_title: OFFICIAL RESULTS
election_title: |
{{capital_election}} Name:
{{title}}
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/domain_model/ITabulators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export interface allocatedScoreResults extends Omit<genericResults, 'tied'> {
votingMethod: 'STAR_PR',
tied: candidate[][],
summaryData: allocatedScoreSummaryData,
logs: tabulatorLog[]
}

export interface approvalResults extends genericResults {
Expand Down

0 comments on commit d91d526

Please sign in to comment.