diff --git a/src/components/ElectionMap.js b/src/components/ElectionMap.js index 86e7f30..cb2957c 100644 --- a/src/components/ElectionMap.js +++ b/src/components/ElectionMap.js @@ -303,10 +303,10 @@ class ElectionMap extends SvgChart { // return match && match.complete ? 0 : this.options().size } - opacity(d) { - return 1 - const match = this.dataLookup[d.id] - return match && match.complete ? 1 : 0.5 + opacity(data) { + const match = this.dataLookup[data.id] + return match && match.opacity != null ? match.opacity : 1 + // return match && match.complete ? 1 : 0.5 } resetZoom() { @@ -375,7 +375,7 @@ class ElectionMap extends SvgChart { .attr("transform", d => `translate(${d.x},${d.y})`) .select("rect") .attr("fill", d => this.color(d.data)) - // .attr("opacity", d => this.opacity(d.data)) + .attr("opacity", d => this.opacity(d.data)) zoneSelection.exit().remove() diff --git a/src/components/PerPartyMapContainer.js b/src/components/PerPartyMapContainer.js index 14c18ef..8533e44 100644 --- a/src/components/PerPartyMapContainer.js +++ b/src/components/PerPartyMapContainer.js @@ -1,38 +1,27 @@ -import React, { useCallback, useContext, useMemo, useState } from "react" -import { - checkFilter, - filters, - zones, - zonePath, - getZoneByProvinceIdAndZoneNo, -} from "../models/information" -import { useSummaryData } from "../models/LiveDataSubscription" +import _ from "lodash" +import React, { useCallback, useMemo } from "react" +import { getSeatDisplayModel } from "../models/ConstituencySeat" +import { zones, getPartyById } from "../models/information" +import { useSummaryData, usePerPartyData } from "../models/LiveDataSubscription" import { isZoneFinished, - shouldDisplayZoneData, nationwidePartyStatsFromSummaryJSON, } from "../models/PartyStats" +import { media, WIDE_NAV_MIN_WIDTH } from "../styles" import ElectionMap, { electionMapLoadingData } from "./ElectionMap" -import ElectionMapTooltip from "./ElectionMapTooltip" import ZoneMark from "./ZoneMark" -import { ZoneFilterContext } from "./ZoneFilterPanel" -import { navigate } from "gatsby" -import { trackEvent } from "../util/analytics" -import { media, WIDE_NAV_MIN_WIDTH } from "../styles" -import { getSeatDisplayModel } from "../models/ConstituencySeat" -import _ from "lodash" /** - * @param {import('../models/LiveDataSubscription').DataState} summaryState + * @param {ElectionDataSource.SummaryJSON | null} summary + * @param {string} partyId + * @param {ReturnType} perPartyModel */ -function getMapData(summaryState, partyId) { - if (!summaryState.completed) { +function getMapData(summary, partyId, perPartyModel) { + if (!summary) { return electionMapLoadingData } else { - /** @type {ElectionDataSource.SummaryJSON} */ - const summary = summaryState.data const row = _.find( - nationwidePartyStatsFromSummaryJSON(summaryState.data), + nationwidePartyStatsFromSummaryJSON(summary), row => row.party.id === +partyId ) if (!row) return electionMapLoadingData @@ -55,12 +44,40 @@ function getMapData(summaryState, partyId) { } return [ ...zones.map((zone, i) => { - const { candidate, zoneStats } = getSeatDisplayModel(summary, zone) - const onMap = candidate && candidate.partyId === partyId + const { candidate: winningCandidate, zoneStats } = getSeatDisplayModel( + summary, + zone + ) + const win = winningCandidate && +winningCandidate.partyId === +partyId + const sentCandidate = perPartyModel.getCandidate( + zone.provinceId, + zone.no + ) + let opacity = 1 + let winningPartyId = "nope" + let complete = false + const interpolate = (value, min = 0, max = 1) => + Math.min(1, Math.max(0, (value - min) / (max - min))) + if (win) { + complete = true + winningPartyId = winningCandidate.partyId + opacity = + 0.5 + + 0.5 * + interpolate( + winningCandidate.score / zoneStats.votesTotal, + 1 / 3, + 1 / 2 + ) + } else if (sentCandidate && winningCandidate) { + winningPartyId = sentCandidate.partyId + opacity = interpolate(sentCandidate.score / winningCandidate.score) + } return { id: `${zone.provinceId}-${zone.no}`, - partyId: onMap ? candidate.partyId : "nope", - complete: onMap && isZoneFinished(zoneStats), + partyId: winningPartyId, + complete: complete, + opacity: opacity, show: true, } }), @@ -69,11 +86,43 @@ function getMapData(summaryState, partyId) { } } +/** + * @param {ElectionDataSource.PerPartyJSON | null} perPartyData + */ +function computePartyCandidateModel(perPartyData) { + const lookupTable = new Map( + perPartyData + ? perPartyData.constituencyCandidates.map(candidate => [ + `${candidate.provinceId}-${candidate.zone}`, + candidate, + ]) + : [] + ) + return { + /** + * @param {number} provinceId + * @param {number} zoneNo + * @return {ElectionDataSource.PerPartyCandidate | undefined} + */ + getCandidate(provinceId, zoneNo) { + return lookupTable.get(`${provinceId}-${zoneNo}`) + }, + } +} + export default function PerPartyMapContainer({ partyId }) { const summaryState = useSummaryData() + const perPartyState = usePerPartyData(partyId) + const party = getPartyById(partyId) + const partyCandidateModel = useMemo( + () => computePartyCandidateModel(perPartyState.data), + [perPartyState.data] + ) const mapData = useMemo( - () => ({ zones: getMapData(summaryState, partyId) }), - [summaryState, partyId] + () => ({ + zones: getMapData(summaryState.data, partyId, partyCandidateModel), + }), + [summaryState.data, partyId, partyCandidateModel] ) const onInit = useCallback(map => {}, []) @@ -92,6 +141,12 @@ export default function PerPartyMapContainer({ partyId }) { }, }} > +
+ + มีผู้สมัครในเขตนั้น   + + ได้รับคะแนนสูงสุดในเขต +
} */ +export function usePerPartyData(partyId) { + const state = useComputed( + () => getLatestDataFileState(`/PerPartyJSON/${partyId}.json`), + [partyId] + ) + return useInertState(state) +} + function useMappedDataState(state, mapper) { return useMemo( () => (state.data ? { ...state, data: mapper(state.data) } : state),