Skip to content

Add pie chart visualization template#595

Open
skaphan wants to merge 23 commits intoartoonie:mainfrom
skaphan:pie-template-clean
Open

Add pie chart visualization template#595
skaphan wants to merge 23 commits intoartoonie:mainfrom
skaphan:pie-template-clean

Conversation

@skaphan
Copy link
Contributor

@skaphan skaphan commented Feb 24, 2026

Summary

  • Add embedded pie chart web component with templates and static assets
  • Add pie chart tab icon and nonblocking template for async loading
  • Build pie chart data from processed Graph (respects config filters)
  • Add hidePie setting following hideSankey/hideTabular pattern
  • Pass config options (textForWinner, excludeFinalWinnerAndEliminatedCandidate) to component
  • Initialize pie chart to final round on load
  • Add 32 tests for graphToRCtab converter
  • Fix tooltip positioning (position:fixed + operator precedence fix)

Response to feedback

Matching the candidate colors with the rest of the visualizations

Done.

The data doesn't seem to match the rest of the visualization. I suspect this is because you're reading directly from the JSON without viewing other config options.

Done. The pie chart now reads from the processed Graph (via a new graphToRCtab converter) instead of raw JSON, so it respects all config filters including excludeFinalWinnerAndEliminatedCandidate.

The other visualizations show Undeclared Write-Ins eliminated first but the pie chart hides UWI altogether.

We haven't been doing anything special about this. Not sure what the issue is.

The pie chart doesn't seem to respect any of the configuration options on the upload page (like "How to refer to the winner", "Hide winner because results are preliminary")

Done. textForWinner and excludeFinalWinnerAndEliminatedCandidate are now passed through from the config.

"Use first-round totals for all percentage calculations"

We do this by default. Do you need it to work from active votes if you don't have it this way?

The pie chart shows the first round on page load instead of the last round

Done.

We should have a setting that allows you to hide the Pie chart

Done. Added hidePie model field, config JS variable, settings checkbox, and tab hiding — following the exact hideSankey/hideTabular pattern. Not heavily tested but it's a mechanical copy of the existing pattern.

We should remove the extraneous buttons that don't match the rest of RCVis, like the "one small step" and the description at the bottom

Got rid of the description at the bottom (now behind a showCaptions prop, default false).

I'm going to push back a little on the "one small step" button. It is a useful feature when you're really trying to see what's going on. The animation is a three-phase thing:

  1. Show the transfers that are going to be made
  2. Move them to the candidates those votes are going to be transferred to
  3. Merge them in with those candidates

We can rename or restyle the button, no problem, but I don't think it's helpful to remove it entirely. But for rcvis, it's really up to you. I will say though that without that, for my purposes I'd rather stick with a version that has it.

I'd like at least cursory tests that step through the visualization to ensure it doesn't break in the future

Added 32 tests for the graphToRCtab converter covering: structure validation, tally correctness, eliminations, multi-winner surplus, inactive ballots, no-threshold elections, the excludeFinalWinnerAndEliminatedCandidate flag, and the _stringify helper.

I'd like to run a PageSpeed Insights test to ensure no degradation in loading times

Please do, but be aware that all we're doing is downloading a piece of code that will be cached for the lifetime of the browser instance, so it only has to happen once per browser instance. The only downloading after that will be the parameters (the color map and JSON file) to be visualized.

@skaphan skaphan force-pushed the pie-template-clean branch from 7b29c2d to 5251937 Compare March 4, 2026 17:57
@artoonie
Copy link
Owner

artoonie commented Mar 4, 2026

I haven't looked at the code, but the interface looks great, only a few small things remain:

  1. Matching the candidate colors with the rest of the visualizations
  2. The data doesn't seem to match the rest of the visualization. I suspect this is because you're reading directly from the JSON without viewing other config options. For example, in this JSON, the other visualizations show Undeclared Write-Ins eliminated first but the pie chart hides UWI altogether.
  3. Similarly, the pie chart doesn't seem to respect any of the configuration options on the upload page (like "How to refer to the winner", " Hide winner because results are preliminary", "Use first-round totals for all percentage calculations"
  4. The pie chart shows the first round on page load instead of the last round
  5. We should have a setting that allows you to hide the Pie chart, just like we can hide the tabular and sankey formats. Some jurisdictions want control over which visualizations are visible.
  6. We should remove the extraneous buttons that don't match the rest of RCVis, like the "one small step" and the description at the bottom
  7. I'd like at least cursory tests that step through the visualization to ensure it doesn't break in the future
  8. I'd like to run a PageSpeed Insights test to ensure no degradation in loading times

@skaphan
Copy link
Contributor Author

skaphan commented Mar 6, 2026

Added 7 new tests for the pie chart feature:

testSimple.py (3 new + 1 updated):

  • test_pie_vistype_renders?vistype=pie embedded URL returns 200
  • test_pie_embedly_redirect/vo/{slug}/pie redirects to /ve/{slug}?vistype=pie
  • test_hide_pie_confighidePie toggle propagates to JavaScript config
  • test_cloudflare_purge — expected URLs updated to include pie paths (/vo/{slug}/pie and ?vistype=pie)

testGraphToRCtab.py (4 new in SpecialCharacterTests class):

  • Tests that candidate names with HTML tags, quotes, commas, and other special characters survive the graph-to-RCTab JSON conversion without corruption

@skaphan
Copy link
Contributor Author

skaphan commented Mar 16, 2026

Regarding the comment "Similarly, the pie chart doesn't seem to respect any of the configuration options on the upload page (like [...] Use first-round totals for all percentage calculations), that is now controllable by a prop passed to the pie-chart component.

@artoonie artoonie temporarily deployed to rcvis-br-pie-template-clean March 19, 2026 22:28 Inactive
@artoonie
Copy link
Owner

Thanks! Some Qs on this version:
image

  1. What is that "eliminate" button under "Play Animation"? It doesn't seem to do anything.
  2. Does this filter out candidates with zero votes? Round 1 here eliminates "Undeclared Write-Ins". I could imagine feasible solutions including either using a minimum size and scaling everything else accordingly, or simply truncating rounds that have no visible actions so the dropdown and animation would begin on Round 2 in this case.

I was able to run a pagespeed insights test. Here's RCVis with the results cached on the server:
image

And here's RCVis with the pie chart, again with the results cached on the server:
image

That almost certainly means the javascript is the bottleneck here. The pagespeed was already on the lower end, but the new value will likely tank RCVis' SEO. Is this something you have the resources to address?

@skaphan
Copy link
Contributor Author

skaphan commented Mar 20, 2026 via email

@skaphan
Copy link
Contributor Author

skaphan commented Mar 20, 2026 via email

skaphan added 14 commits March 20, 2026 18:11
Add embedded pie chart web component, templates, static assets,
and pie_chart_no_cache setting. Store raw JSON on graph for pie
chart data access.
The pie chart was the only visualization reading graph._raw_JSON directly,
bypassing config filters like excludeFinalWinnerAndEliminatedCandidate.
New converter in visualizer/pie/graphToRCtab.py builds RCTab-compatible
JSON from the same processed Graph that bar chart and sankey use.

Co-Authored-By: Claude Opus 4.6
- Pass textForWinner and excludeFinalWinnerAndEliminatedCandidate
- Set currentRound to numRounds so pie loads on last round
- Rebuilt standalone component with new props

Co-Authored-By: Claude Opus 4.6
Following the hideSankey/hideTabular pattern: model field, config JS
variable, settings checkbox, and tab hiding in hideTabsBasedOnConfig().

Co-Authored-By: Claude Opus 4.6
Covers structure validation, tally correctness, eliminations, multi-winner
surplus, inactive ballots, no-threshold elections, excludeFinalWinner flag,
and the _stringify helper.

Co-Authored-By: Claude Opus 4.6
Co-Authored-By: Claude Opus 4.6
Use a coprime stride near N/2 to reorder the color palette so that
visually similar colors are spread apart around the pie chart.

Co-Authored-By: Claude Opus 4.6
Replace color scramble with tally interleaving: alternate largest and
smallest candidates so small slices always have a large neighbor,
reducing label overlap. Inactive Ballots always last.

Fix color generation to use actual tally entry count instead of
config.numCandidates, which excluded Inactive Ballots and caused
color wrapping on large elections.

Co-Authored-By: Claude Opus 4.6
Per-round reordering broke D3 pie animation because candidate positions
shifted between rounds. Now the interleave order is stable across all
rounds, just dropping eliminated candidates.

Co-Authored-By: Claude Opus 4.6
Tests pie vistype rendering, embedly redirect, hidePie config toggle,
and cloudflare cache purge including pie URLs. Tests graphToRCtab with
candidate names containing HTML, quotes, and other special characters.

Co-Authored-By: Claude Opus 4.6
Set firstRoundDeterminesPercentages=false so percentages use active
votes per round as denominator. Inactive Ballots shows count only.
Animation button cycles Eliminate/Transfer/Consolidate labels.

Co-Authored-By: Claude Opus 4.6
…hart

Tooltip now positions near mouse cursor with viewport clamping.
Pie chart centered (removed stale margin-right). Bubble visualization
references removed.

Co-Authored-By: Claude Opus 4.6
@skaphan skaphan force-pushed the pie-template-clean branch from b24e9c3 to c9b41ed Compare March 20, 2026 19:23
@skaphan
Copy link
Contributor Author

skaphan commented Mar 21, 2026 via email

@skaphan
Copy link
Contributor Author

skaphan commented Mar 21, 2026 via email

@artoonie
Copy link
Owner

Claude is mistaken here — both rest were run twice, once to cache on the server and a second time to use the cached result. Heroku cold start isn’t an issue: the server sleeps after 30m of inactivity, which isn’t the case here.

There’s debate on whether FCP directly or indirectly affects SEO, but in my experience it definitely has an impact: https://www.reddit.com/r/SEO/s/NDTmkmpSjP

as to why the “SEO” score went up, that’s a fluke: on the first one, the visualization was uploaded by a private account so the headers tell robots to not index the page. The SEO score is of course low for any page that asks to not show up on search engines :)

I can try to dig deeper into improving this score next week. Some tricks the other visualizations use is first figuring out the expected visualization size before doing any other work and setting up all div sizing — that reduces cumulative layout shift significantly. FCP is a little harder.

@skaphan
Copy link
Contributor Author

skaphan commented Mar 22, 2026 via email

Set aspect-ratio and max-width on pie-body container so the
browser allocates space before the custom element renders.
Reduces Cumulative Layout Shift from 0.753 to 0.014.

Co-Authored-By: Claude Opus 4.6
@skaphan
Copy link
Contributor Author

skaphan commented Mar 22, 2026 via email

@skaphan
Copy link
Contributor Author

skaphan commented Mar 23, 2026 via email

@artoonie artoonie temporarily deployed to rcvis-pr-595 March 24, 2026 15:47 Inactive
@artoonie artoonie temporarily deployed to rcvis-pr-595 March 24, 2026 18:05 Inactive
@artoonie
Copy link
Owner

artoonie commented Mar 24, 2026

Continuing on the visual review based on our call:

  • Resolved: all upload options seem to be respected now 💯
  • PageSpeed Insights now looks equivalent to the current RCVis state 💯

Other issues I've seen:

  1. The "eliminate" button doesn't work on my end
  2. Names get truncated, see link: https://rcvis-pr-613.herokuapp.com/v/portland-2015-mayoral-race-2#pie
image
  1. Linter issues:
************* Module common.viewUtils
common/viewUtils.py:72:19: W0212: Access to a protected member _raw_JSON of a client class (protected-access)
************* Module rcvis.settings
rcvis/settings.py:196:32: W0613: Unused argument 'path' (unused-argument)
************* Module visualizer.pie.graphToRCtab
visualizer/pie/graphToRCtab.py:24:0: R0914: Too many local variables (30/15) (too-many-locals)
visualizer/pie/graphToRCtab.py:39:4: C0103: Variable name "num_rounds" doesn't conform to camelCase naming style (invalid-name)
visualizer/pie/graphToRCtab.py:42:4: C0103: Variable name "inactive_names" doesn't conform to camelCase naming style (invalid-name)
visualizer/pie/graphToRCtab.py:43:4: C0103: Variable name "num_active_candidates" doesn't conform to camelCase naming style (invalid-name)
visualizer/pie/graphToRCtab.py:49:4: C0103: Variable name "round1_nodes" doesn't conform to camelCase naming style (invalid-name)
visualizer/pie/graphToRCtab.py:51:4: C0103: Variable name "inactive_order" doesn't conform to camelCase naming style (invalid-name)
visualizer/pie/graphToRCtab.py:58:4: C0103: Variable name "interleaved_order" doesn't conform to camelCase naming style (invalid-name)
visualizer/pie/graphToRCtab.py:70:8: C0103: Variable name "nodes_this_round" doesn't conform to camelCase naming style (invalid-name)
visualizer/pie/graphToRCtab.py:71:8: C0103: Variable name "names_this_round" doesn't conform to camelCase naming style (invalid-name)
visualizer/pie/graphToRCtab.py:82:8: C0103: Variable name "tally_results" doesn't conform to camelCase naming style (invalid-name)
visualizer/pie/graphToRCtab.py:84:12: C0103: Variable name "tally_result" doesn't conform to camelCase naming style (invalid-name)
visualizer/pie/graphToRCtab.py:96:16: C0103: Variable name "target_candidate" doesn't conform to camelCase naming style (invalid-name)
visualizer/pie/graphToRCtab.py:104:8: C0103: Variable name "inactive_ballots" doesn't conform to camelCase naming style (invalid-name)
visualizer/pie/graphToRCtab.py:124:4: C0103: Variable name "first_round_total" doesn't conform to camelCase naming style (invalid-name)
visualizer/pie/graphToRCtab.py:126:4: C0103: Variable name "rctab_json" doesn't conform to camelCase naming style (invalid-name)
visualizer/pie/graphToRCtab.py:24:0: R0912: Too many branches (18/12) (too-many-branches)
visualizer/pie/graphToRCtab.py:24:0: R0915: Too many statements (53/50) (too-many-statements)
************* Module visualizer.tests.testGraphToRCtab
visualizer/tests/testGraphToRCtab.py:17:4: C0116: Missing function or method docstring (missing-function-docstring)
visualizer/tests/testGraphToRCtab.py:20:4: C0116: Missing function or method docstring (missing-function-docstring)
visualizer/tests/testGraphToRCtab.py:23:4: C0116: Missing function or method docstring (missing-function-docstring)
visualizer/tests/testGraphToRCtab.py:26:4: C0116: Missing function or method docstring (missing-function-docstring)
visualizer/tests/testGraphToRCtab.py:29:4: C0116: Missing function or method docstring (missing-function-docstring)
visualizer/tests/testGraphToRCtab.py:45:4: C0116: Missing function or method docstring (missing-function-docstring)
visualizer/tests/testGraphToRCtab.py:51:4: C0116: Missing function or method docstring (missing-function-docstring)
visualizer/tests/testGraphToRCtab.py:54:4: C0116: Missing function or method docstring (missing-function-docstring)
visualizer/tests/testGraphToRCtab.py:63:4: C0116: Missing function or method docstring (missing-function-docstring)
visualizer/tests/testGraphToRCtab.py:72:8: C0103: Variable name "tally_results" doesn't conform to camelCase naming style (invalid-name)
visualizer/tests/testGraphToRCtab.py:76:4: C0116: Missing function or method docstring (missing-function-docstring)
visualizer/tests/testGraphToRCtab.py:90:4: C0116: Missing function or method docstring (missing-function-docstring)
visualizer/tests/testGraphToRCtab.py:93:4: C0116: Missing function or method docstring (missing-function-docstring)
visualizer/tests/testGraphToRCtab.py:99:8: C0103: Variable name "graph_candidates" doesn't conform to camelCase naming style (invalid-name)
visualizer/tests/testGraphToRCtab.py:100:8: C0103: Variable name "first_tally" doesn't conform to camelCase naming style (invalid-name)
visualizer/tests/testGraphToRCtab.py:107:8: C0103: Variable name "round_2_tally" doesn't conform to camelCase naming style (invalid-name)
visualizer/tests/testGraphToRCtab.py:108:8: C0103: Variable name "round_3_tally" doesn't conform to camelCase naming style (invalid-name)
visualizer/tests/testGraphToRCtab.py:114:8: C0103: Variable name "all_eliminated" doesn't conform to camelCase naming style (invalid-name)
visualizer/tests/testGraphToRCtab.py:121:4: C0116: Missing function or method docstring (missing-function-docstring)
visualizer/tests/testGraphToRCtab.py:122:8: C0103: Variable name "last_round" doesn't conform to camelCase naming style (invalid-name)
visualizer/tests/testGraphToRCtab.py:134:4: C0116: Missing function or method docstring (missing-function-docstring)
visualizer/tests/testGraphToRCtab.py:138:4: C0116: Missing function or method docstring (missing-function-docstring)
visualizer/tests/testGraphToRCtab.py:154:4: C0116: Missing function or method docstring (missing-function-docstring)
visualizer/tests/testGraphToRCtab.py:159:8: C0103: Variable name "all_elected" doesn't conform to camelCase naming style (invalid-name)
visualizer/tests/testGraphToRCtab.py:176:8: C0103: Variable name "inactive_names" doesn't conform to camelCase naming style (invalid-name)
visualizer/tests/testGraphToRCtab.py:177:8: C0103: Variable name "real_count" doesn't conform to camelCase naming style (invalid-name)
visualizer/tests/testGraphToRCtab.py:192:8: C0103: Variable name "first_round" doesn't conform to camelCase naming style (invalid-name)
visualizer/tests/testGraphToRCtab.py:198:8: C0103: Variable name "last_round" doesn't conform to camelCase naming style (invalid-name)
visualizer/tests/testGraphToRCtab.py:217:4: C0116: Missing function or method docstring (missing-function-docstring)
visualizer/tests/testGraphToRCtab.py:236:8: C0103: Variable name "last_round_nodes" doesn't conform to camelCase naming style (invalid-name)
visualizer/tests/testGraphToRCtab.py:250:8: C0103: Variable name "last_round" doesn't conform to camelCase naming style (invalid-name)
visualizer/tests/testGraphToRCtab.py:261:8: C0103: Variable name "last_round" doesn't conform to camelCase naming style (invalid-name)
visualizer/tests/testGraphToRCtab.py:281:8: C0103: Variable name "first_tally" doesn't conform to camelCase naming style (invalid-name)
visualizer/tests/testGraphToRCtab.py:282:8: C0103: Variable name "html_names" doesn't conform to camelCase naming style (invalid-name)
visualizer/tests/testGraphToRCtab.py:288:8: C0103: Variable name "first_tally" doesn't conform to camelCase naming style (invalid-name)
visualizer/tests/testGraphToRCtab.py:293:8: C0103: Variable name "graph_candidates" doesn't conform to camelCase naming style (invalid-name)
visualizer/tests/testGraphToRCtab.py:294:8: C0103: Variable name "first_tally_names" doesn't conform to camelCase naming style (invalid-name)
-----------------------------------
Your code has been rated at 9.91/10
These files must be perfectly linted
Checking for autopep8 differences
-----> test command `./scripts/run-tests.sh` failed with exit status 1
  1. Even with an elimination on Round 1, there seems to be a 4-second gap before the animation begins. Again: https://rcvis-pr-613.herokuapp.com/v/portland-2015-mayoral-race-2#pie

I'll take a look at the code now as well

@skaphan
Copy link
Contributor Author

skaphan commented Mar 24, 2026 via email

Copy link
Owner

@artoonie artoonie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks good, only minor requests here

headers['Cache-Control'] = 'no-cache'


WHITENOISE_ADD_HEADERS_FUNCTION = pie_chart_no_cache
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed or was this just for testing?

#pie-body {
max-width: 800px;
width: 100%;
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a TODO here -- is it needed?

class Migration(migrations.Migration):

dependencies = [
('visualizer', '0032_jsonconfig_forcefirstrounddeterminespercentages'),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will need to be bumped to 33 and the file renamed to 34 to avoid the merge conflict with the caching PR


def test_string_passthrough(self):
""" RCTab JSON sometimes has string values already """
self.assertEqual(_stringify("100"), "100")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how useful these tests are (the tests are longer than the code itself, which is easy to read) -- but, if we want to test them, best to move this function to visualizer/common.py and remove the _

def test_config_has_contest(self):
self.assertIn('contest', self.result['config'])
self.assertIsInstance(self.result['config']['contest'], str)
self.assertGreater(len(self.result['config']['contest']), 0)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what this class is testing. It seems to just test that the JSON is read correctly, but nothing to do with the pie chart?

graph_candidates = {c.name for c in self.graph.candidates}
first_tally_names = set(self.result['results'][0]['tally'].keys())
self.assertTrue(graph_candidates.issubset(first_tally_names),
f"Missing candidates: {graph_candidates - first_tally_names}")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This entire file is a little confusing, it seems to basically just test JSON loading? And none of it is related to the pie chart? It seems safe to remove entirely.

with self.settings(CACHES={'default': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}}):
response = self.client.get(path)
self.assertContains(response, 'config.hidePie = true')
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't mind one additional test that views the pie chart, changes rounds, and asserts that some other round's text is now on the screen -- just as a lightweight test of the animation. This is done for other visualizations so I hope Claude can easily whip something up using existing functionality.

.gitignore Outdated
!.elasticbeanstalk/*.global.yml

**/**/dist/
scripts/reset-db.sh
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Best to keep this in a local .gitignore_global or similar since it's not produced by rcvis or any of its dependencies

- Make pie_chart_no_cache conditional on DEBUG
- Remove TODO comment from pie.css
- Renumber hidePie migration to 0034 (avoids conflict with caching PR)
- Move _stringify to common.stringify (public API)
- Improve testGraphToRCtab docstring explaining its purpose
- Remove scripts/reset-db.sh from .gitignore
- Add pie chart round-change Selenium test

Co-Authored-By: Claude Opus 4.6
@skaphan
Copy link
Contributor Author

skaphan commented Mar 24, 2026 via email

@skaphan
Copy link
Contributor Author

skaphan commented Mar 25, 2026 via email

@artoonie
Copy link
Owner

My display is 3008x1692. When I move RCVis to a smaller screen, the larger button now works sometimes but not reliably. It seems the target area is very small? Zooming in on the large screen seems to fix it too.

@artoonie
Copy link
Owner

Also: the button is active in the final round, which is the default state, but doesn't do anything in the final round. The button should be disabled, or it should "restart" the current round, or it should be a different type of interaction.

I'm not sure a button is the right move here anyway, now that I'm getting to use it for the first time. For example, a "stepper" might work better:
image
(Image found here: https://dribbble.com/search/stepper)

@skaphan
Copy link
Contributor Author

skaphan commented Mar 25, 2026 via email

@skaphan
Copy link
Contributor Author

skaphan commented Mar 25, 2026 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants