mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-12 21:38:35 +03:00
Feature/add coloring algorithm (#169)
* Fixed bug: when all child filters unselected, turn off parent as well * Refactored placement of onSelectFilter to be in Layout; working logic for updating coloring sets * Linting fixes and removal of console logs * Added separate component for colored markers which clusters and events will use; working calculation of color percentages based off of coloringset * Working colors for clusters; need to implement for individual points as well * Adding two new features to select whether to color by association or by category (can't do both) * Working colors for filter list panel; text and checkbox change according to colorset groupings * Working timeline events with coloring algorithm * Handle select acts different on map when we don't render all points and only filter through clusters; can fix this by not filtering before passing in locations to events in map * Removed extraneous prop * Working point count on hover again; numbers were showing up below the colored markers * Linting fixes and minor refactor of calculateColorPercentage for linting to ass * Comments and more linting fixes * add dev command for windows subsystem for linux * return default styles for category toggles * dynamically filter out timelines * calibrate styling * further calibrations * correct contrast * lint Co-authored-by: efarooqui <efarooqui@pandora.com> Co-authored-by: Lachlan Kermode <lachiekermode@gmail.com>
This commit is contained in:
@@ -11,6 +11,28 @@ export function calcDatetime (date, time) {
|
||||
return dt.toDate()
|
||||
}
|
||||
|
||||
export function getCoordinatesForPercent (radius, percent) {
|
||||
const x = radius * Math.cos(2 * Math.PI * percent)
|
||||
const y = radius * Math.sin(2 * Math.PI * percent)
|
||||
return [x, y]
|
||||
}
|
||||
|
||||
/**
|
||||
* This function takes the array of percentages: [0.5, 0.5, ...]
|
||||
* and maps it by index to the set of colors ['#fff', '#000', ...]
|
||||
* If there aren't enough colors in the set, it raises an error for the user
|
||||
*
|
||||
* Return value:
|
||||
* ex. {'#fff': 0.5, '#000': 0.5, ...} */
|
||||
export function zipColorsToPercentages (colors, percentages) {
|
||||
if (colors.length < percentages.length) throw new Error('You must declare an appropriate number of filter colors')
|
||||
|
||||
return percentages.reduce((map, percent, idx) => {
|
||||
map[colors[idx]] = percent
|
||||
return map
|
||||
}, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URI params to start with predefined set of
|
||||
* https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
|
||||
@@ -63,6 +85,48 @@ export function trimAndEllipse (string, stringNum) {
|
||||
return string
|
||||
}
|
||||
|
||||
/**
|
||||
* From the set of associations, grab a given filter's set of parents,
|
||||
* ie. all the elements in the path array before the idx where the filter is located.
|
||||
* If we can't find the filter by the ID, we know its a meta filter, so we look
|
||||
* through every association's given path attribute to find its location.
|
||||
*
|
||||
* Returns the list of parents: ex. ['Chemical', 'Tear Gas', ...]
|
||||
*/
|
||||
export function getFilterParents (associations, filter) {
|
||||
for (let a of associations) {
|
||||
const { filter_paths: fp } = a
|
||||
if (a.id === filter) {
|
||||
return fp.slice(0, fp.length - 1)
|
||||
}
|
||||
const filterIndex = fp.indexOf(filter)
|
||||
if (filterIndex === 0) return []
|
||||
if (filterIndex > 0) return fp.slice(0, filterIndex)
|
||||
}
|
||||
throw new Error('Attempted to get parents of nonexistent filter')
|
||||
}
|
||||
|
||||
/**
|
||||
* Grabs the second to last element in the paths array for a given existing filter.
|
||||
* This is the filter's most immediate ancestor.
|
||||
*/
|
||||
export function getImmediateFilterParent (associations, filter) {
|
||||
const parents = getFilterParents(associations, filter)
|
||||
if (parents.length === 0) return null
|
||||
return parents[parents.length - 1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Grabs a given filter's siblings: the set of associations that share the same immediate filter parent.
|
||||
*/
|
||||
export function getFilterSiblings (allFilters, filterParent, filterKey) {
|
||||
return allFilters.reduce((acc, val) => {
|
||||
const valParent = getImmediateFilterParent(allFilters, val.id)
|
||||
if (valParent === filterParent && val.id !== filterKey) acc.push(val.id)
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
|
||||
export function getEventCategories (event, categories) {
|
||||
const matchedCategories = []
|
||||
if (event.associations && event.associations.length > 0) {
|
||||
@@ -180,7 +244,7 @@ export function calcOpacity (num) {
|
||||
* other events there are in the same render. The idea here is that the
|
||||
* overlaying of events builds up a 'heat map' of the event space, where
|
||||
* darker areas represent more events with proportion */
|
||||
const base = num >= 1 ? 0.6 : 0
|
||||
const base = num >= 1 ? 0.9 : 0
|
||||
return base + (Math.min(0.5, 0.08 * (num - 1)))
|
||||
}
|
||||
|
||||
@@ -188,14 +252,16 @@ export function calcClusterOpacity (pointCount, totalPoints) {
|
||||
/* Clusters represent multiple events within a specific radius. The darker the cluster,
|
||||
the larger the number of underlying events. We use a multiplication factor (50) here as well
|
||||
to ensure that the larger clusters have an appropriately darker shading. */
|
||||
return Math.min(0.85, 0.08 + (pointCount / totalPoints) * 50)
|
||||
const base = 0.5
|
||||
return base + Math.min(0.95, 0.08 + (pointCount / totalPoints) * 50)
|
||||
}
|
||||
|
||||
export function calcClusterSize (pointCount, totalPoints) {
|
||||
/* The larger the cluster size, the higher the count of points that the cluster represents.
|
||||
Just like with opacity, we use a multiplication factor to ensure that clusters with higher point
|
||||
counts appear larger. */
|
||||
return Math.min(50, 10 + (pointCount / totalPoints) * 150)
|
||||
const maxSize = totalPoints > 60 ? 40 : 20
|
||||
return Math.min(maxSize, 10 + (pointCount / totalPoints) * 150)
|
||||
}
|
||||
|
||||
export function isLatitude (lat) {
|
||||
@@ -214,6 +280,59 @@ export function mapClustersToLocations (clusters, locations) {
|
||||
}, [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Loops through a set of either locations or events
|
||||
* and calculates the proportionate percentage of every given association in relation to the coloring set
|
||||
*/
|
||||
export function calculateColorPercentages (set, coloringSet) {
|
||||
if (coloringSet.length === 0) return [1]
|
||||
const associationMap = {}
|
||||
|
||||
for (const [idx, value] of coloringSet.entries()) {
|
||||
for (let filter of value) {
|
||||
associationMap[filter] = idx
|
||||
}
|
||||
}
|
||||
|
||||
const associationCounts = new Array(coloringSet.length)
|
||||
associationCounts.fill(0)
|
||||
|
||||
let totalAssociations = 0
|
||||
|
||||
set.forEach(item => {
|
||||
let innerSet = 'events' in item ? item.events : item
|
||||
|
||||
if (!Array.isArray(innerSet)) innerSet = [innerSet]
|
||||
|
||||
innerSet.forEach(val => {
|
||||
val.associations.forEach(a => {
|
||||
const idx = associationMap[a]
|
||||
if (!idx && idx !== 0) return
|
||||
associationCounts[idx] += 1
|
||||
totalAssociations += 1
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
if (totalAssociations === 0) return [1]
|
||||
|
||||
return associationCounts.map(count => count / totalAssociations)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the idx of a given filter in relation to its position in the coloring set
|
||||
*
|
||||
* Example coloringSet = [['Chemical', 'Tear Gas'], ['Procedural', 'Destruction of property']]
|
||||
*/
|
||||
export function getFilterIdxFromColorSet (filter, coloringSet) {
|
||||
let filterIdx = -1
|
||||
coloringSet.map((set, idx) => {
|
||||
const foundIdx = set.indexOf(filter)
|
||||
if (foundIdx !== -1) filterIdx = idx
|
||||
})
|
||||
return filterIdx
|
||||
}
|
||||
|
||||
export const dateMin = function () {
|
||||
return Array.prototype.slice.call(arguments).reduce(function (a, b) {
|
||||
return a < b ? a : b
|
||||
|
||||
Reference in New Issue
Block a user