diff --git a/src/actions/slidable.js b/src/actions/slidable.js new file mode 100644 index 0000000..a5cc651 --- /dev/null +++ b/src/actions/slidable.js @@ -0,0 +1,51 @@ +export function slidable(node) { + let x; + let left; + + function handleMousedown(event) { + x = event.clientX; + + node.dispatchEvent( + new CustomEvent('slidestart', { + detail: { x }, + }) + ); + + window.addEventListener('mousemove', handleMousemove); + window.addEventListener('mouseup', handleMouseup); + } + + function handleMousemove(event) { + const dx = event.clientX - x; + x = event.clientX; + + node.dispatchEvent( + new CustomEvent('slide', { + detail: { x, dx }, + }) + ); + } + + function handleMouseup(event) { + x = event.clientX; + left = node.offsetLeft; + + node.dispatchEvent( + new CustomEvent('slideend', { + detail: { x, left }, + }) + ); + + window.removeEventListener('mousemove', handleMousemove); + window.removeEventListener('mouseup', handleMouseup); + } + + node.addEventListener('mousedown', handleMousedown); + + return { + destroy() { + node.removeEventListener('mousedown', handleMousedown); + }, + }; + } + \ No newline at end of file diff --git a/src/lib/components/Controls.svelte b/src/lib/components/Controls.svelte index 1a2a540..89a70ae 100644 --- a/src/lib/components/Controls.svelte +++ b/src/lib/components/Controls.svelte @@ -1,14 +1,20 @@ {#if cases} + $attributionScoreFilter = e.detail} /> + // a custom slider for score selection + import { createEventDispatcher } from 'svelte'; + import { scaleLinear } from 'd3-scale'; + import { slidable } from '../../actions/slidable'; + + export let lockInMode = true; + export let label = ''; + export let showLabel = true; + export let min = 0; + export let max = 10; + export let value = [0, 10]; + export let showHandleLabels = true; + export let startColor = 'white'; + export let middleColor = null; + export let stopColor = 'red'; + export let barOpacity = 1; + export let showBorder = true; + + const dispatch = createEventDispatcher(); + const handleWidth = 17; + + const pos = { + left: 0, + right: 0 + }; + + let sliderWidth = 0; + + function handleSlide(e, side) { + const newPos = pos[side] + e.detail.dx; + + if (newPos < 0 || newPos > sliderWidth) return; + if (side === 'left' && newPos > pos.right) return; + if (side === 'left' && newPos < scale.range()[0]) return; + if (side === 'right' && newPos < pos.left) return; + if (side === 'right' && newPos > scale.range()[1]) return; + + pos[side] = newPos; + } + + function handleSlideEnd(e, side) { + if (lockInMode) { + dispatch('changed', [Math.round(scale.invert(pos.left), 0), + Math.round(scale.invert(pos.right), 0)]); + } else { + dispatch('changed', [scale.invert(pos.left), scale.invert(pos.right)]); + } + } + + $: scale = scaleLinear() + .domain([min, max]) + .range([handleWidth / 2, sliderWidth - 1.7 * handleWidth]); + + $: pos.left = scale(value[0]) || 0; + $: pos.right = scale(value[1]) || 0; + + $: background = `linear-gradient(90deg, ${startColor}, ${middleColor ? middleColor + ', ' : ''}${stopColor})`; + + +
+ {#if (showLabel)} +
+ {label} +
+ {/if} +
+
+
handleSlide(e, 'left')} + on:slideend={(e) => handleSlideEnd(e, 'left')}> + {showHandleLabels ? Math.round(scale.invert(pos.left), 0) : ''} +
+
handleSlide(e, 'right')} + on:slideend={(e) => handleSlideEnd(e, 'right')}> + {showHandleLabels ? Math.round(scale.invert(pos.right), 0) : ''} +
+
+
+ + diff --git a/src/lib/utils/colors.js b/src/lib/utils/colors.js new file mode 100644 index 0000000..cee3552 --- /dev/null +++ b/src/lib/utils/colors.js @@ -0,0 +1,11 @@ +export const bg = '#F9F8F8'; +export const usaBlue = '#3c3b6e'; +export const usaRed = '#b22234'; +export const usaLightRed = '#b22234'; +export const usaLightLightRed = '#dbb6b6'; + +export const polBlue = '#2e64a0'; +export const polLightBlue = '#61a3de'; +export const polPurple = '#96659e'; +export const polLightRed = '#a15552'; +export const polRed = '#ca0800'; \ No newline at end of file diff --git a/src/lib/utils/misc.js b/src/lib/utils/misc.js index f994f36..fc37cac 100644 --- a/src/lib/utils/misc.js +++ b/src/lib/utils/misc.js @@ -1,3 +1,5 @@ +import { min, max } from 'd3-array'; + // consistent sort function export const sortConsistently = (itemA, itemB, property, key) => { let valueA = itemA[property]; @@ -31,4 +33,16 @@ export const splitString = (s) => { // check if there's overlap between array and filter export const haveOverlap = (filter, arr) => - filter.filter((d) => d.selected).map((d) => d.id).some((item) => arr.includes(item)); \ No newline at end of file + filter.filter((d) => d.selected).map((d) => d.id).some((item) => arr.includes(item)); + +// extract attribution date range from data +export const getTimeRange = (data) => { + console.log(data) + const maxAttributionDate = max(data, (d) => d.attribution_date); + return([min(data, (d) => d.attribution_date), new Date( + maxAttributionDate.getFullYear(), maxAttributionDate.getMonth() + 5 + )]); +}; + +// check if a number is within a 2D range (given as array with length 2) +export const withinRange = (arr, num, bypass = false) => bypass ? true : (num >= arr[0] && num <= arr[1]); \ No newline at end of file diff --git a/src/lib/utils/scales.js b/src/lib/utils/scales.js new file mode 100644 index 0000000..8aa1efd --- /dev/null +++ b/src/lib/utils/scales.js @@ -0,0 +1,25 @@ +import { timeScale, attributionScoreScale} from '../../stores/scales'; + import { + usaRed, + polBlue, + polPurple, + polRed } from '$lib/utils/colors'; + import { scaleTime, scaleLinear } from 'd3-scale'; + import { max } from 'd3-array' + //import { getTimeRange } from './misc'; + + // sets all the basic scales + export const setScales = (data, width, margin) => { + if (!data) return; + + // time scale + /*timeScale.set(scaleTime() + .domain(getTimeRange(data)) + .range([margin.left, width - margin.right]));*/ + + // attribution score scale + attributionScoreScale.set(scaleLinear() + .domain([-1, 1.1 * max(data, (d) => d.attributionScore)]) + .range(['#FFFFFF', usaRed])); + }; + \ No newline at end of file diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 17407be..6a97bc5 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -7,14 +7,17 @@ import CaseTable from '$lib/components/CaseTable.svelte'; import Timeline from '$lib/components/Timeline.svelte'; import Controls from '$lib/components/Controls.svelte'; - import { splitString, haveOverlap } from '$lib/utils/misc' + import { splitString, haveOverlap, withinRange } from '$lib/utils/misc' + import { setScales } from '$lib/utils/scales'; import { platformFilter, actorNationFilter, sourceFilter, sourceCategoryFilter, - methodFilter + methodFilter, + attributionScoreFilter, + attributionScoreDef } from '../stores/filters'; let cases = []; @@ -27,6 +30,8 @@ d.platform = splitString(d.platform) d.actor_nation = splitString(d.actor_nation) d.methods = splitString(d.methods) + d.attribution_total_score = +d.attribution_total_score + d.attribution_date = new Date(d.attribution_date) d.show = false }) @@ -35,8 +40,9 @@ sourceFilter.init(cases, 'source') sourceCategoryFilter.init(cases, 'source_category') methodFilter.init(cases, 'methods') + $attributionScoreFilter = attributionScoreDef; - console.log(cases.map(d => d.methods)) + //console.log(cases.map(d => d.attribution_date)) }); $: if (cases) { @@ -47,9 +53,21 @@ && haveOverlap($sourceFilter, d.source) && haveOverlap($sourceCategoryFilter, d.source_category) && haveOverlap($methodFilter, d.methods) + && withinRange($attributionScoreFilter, d.attribution_total_score) })) + } + + let width = 1200 + let margin = { + top: 30, + right: 30, + bottom: 30, + left: 30 } + // set the scales + $: setScales(cases, width, margin); +
diff --git a/src/stores/scales.js b/src/stores/scales.js new file mode 100644 index 0000000..427ca22 --- /dev/null +++ b/src/stores/scales.js @@ -0,0 +1,6 @@ +import { writable, readable } from 'svelte/store'; + +export const timeScale = writable(); +export const attributionScoreScale = writable(); + +//export const scaleFactor = readable(window.devicePixelRatio || 1); \ No newline at end of file