Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

เกิดอยากลองทำ Heatmap ขึ้นมา #246

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

dtinth
Copy link
Contributor

@dtinth dtinth commented Mar 27, 2019

เอาไว้ดูภาพรวมคะแนนเสียงของแต่ละพรรค

image

Before After
พท
image
 
image
พปชร
image
 
image
อนค
image
 
image
ปชป
image
 image
ภท
image
 image

@p16i
Copy link
Contributor

p16i commented Mar 27, 2019

Currently, the distraction between the symbols of a party has no candidate in that zone or only few vote received.

I would suggest 2 things:

  1. use a different symbol for no candidate. It could be ☐.
  2. don't not set opacity to zero for receiving zero vote.

@dtinth
Copy link
Contributor Author

dtinth commented Mar 27, 2019

@heytitle I agree with 1), but for 2) I think for zones whose candidate received almost no votes, I prefer to make it obvious. Otherwise, it becomes hard to distinguish between having some votes and having almost no votes. i.e. it’s more obvious which region doesn’t support which party.

@p16i
Copy link
Contributor

p16i commented Mar 27, 2019

ok, that's reasonable. let's see how it looks.

return match && match.complete ? 1 : 0.5
opacity(data) {
const match = this.dataLookup[data.id]
return match && match.opacity != null ? match.opacity : 1
Copy link
Collaborator

Choose a reason for hiding this comment

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

The opacity computation should happen within this file, not the container. To separate visual encoding logic from data.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm… I have to disagree (quite strongly) on this one, because as the ElectionMap gets re-used to display multiple aspects of the election result, the code in this file will become increasingly complex, trying to account for all the visualizations.

e.g.

  if (!match) {
    return 1
  }
  if (match.displayMode === 'heatmap') {
    if (match.areaStatus === 'winning') {
      return scaleLinear(/* ... */)
    } else if (match.areaStatus === 'sent') {
      return scaleLinear(/* ... */)
// (...so on...)

Not only the logic, but the interface would become more complex. for example, here’s what would happen to IMapZone to make election map have all the data necessary to compute the opacity:

 interface IMapZone {
   id: number
   partyId: number
   complete: boolean
   show: boolean
+  displayMode: 'heatmap' | 'winningCandidate'
+  areaStatus?: 'winning' | 'sent' | null
+  displayedCandidateScore?: number
+  winningCandidateScore?: number
+  totalVotersCount?: number
 }

Creating more visualizations would require editing code at 2 places — modifying the container to pass the required data to ElectionMap, and modifying the ElectionMap (which is 400+ lines already) to be able to make sense of that data. This shows a clear violation of the open-closed principle…

In fact, in my opinion, the ElectionMap would be easier to use and understand how did it become a more dumb component. i.e. it receives only what it needs to know to display it, and displays it, e.g.:

 interface IMapZone {
   id: number
-  partyId: number
-  complete: boolean
-  show: boolean
+  fill: 'solid' | 'striped'
+  color: string
+  opacity: number
 }

This would allow more visualization to be created just by creating a new container with little to no modification to the ElectionMap.

Copy link
Collaborator

@kristw kristw Mar 28, 2019

Choose a reason for hiding this comment

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

fair enough. agree with passing the computed encoding can be more convenient as the logic for computation is complex.

Another pattern that many d3-based components use is to pass

  • function color(datum){}
  • function opacity(datum){}
  • function color(datum){}
    as separate arguments
    (When using d3kit these can be passed via options)
    and pass data with all the necessary fields.

The pros of this approach are

  • The datum contains all raw fields. If there is any new encoding channel (e.g. strokeColor) that does not require new field in the raw data, only new encoding function is needed.
  • The datum is usually passed to the event listener, e.g. mouseenter that tooltip use. Having the raw data item can be convenient for display instead of having to perform lookup again.
  • matches general visualization mental model that map data to visual encoding channel.

The cons

  • precomputing the channels can be more optimized if they shared computation logic.

let opacity = 1
let winningPartyId = "nope"
let complete = false
const interpolate = (value, min = 0, max = 1) =>
Copy link
Collaborator

Choose a reason for hiding this comment

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

use d3.scaleLinear or more specifically import { scaleLinear } from 'd3-scale' will save you many lines.

Also worth considering scaleQuantize instead to provide steps of colors instead of continuous range.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for your suggestion — will do that.

@TOTeGuard

This comment has been minimized.

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.

4 participants