Этот коммит содержится в:
Maarten 2024-09-27 12:16:54 +02:00
родитель 8ca51e21ea
Коммит 22052a9151
8 изменённых файлов: 300 добавлений и 5 удалений

51
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);
},
};
}

Просмотреть файл

@ -1,14 +1,20 @@
<script> <script>
import Dropdown from '$lib/components/Dropdown.svelte'; import Dropdown from '$lib/components/Dropdown.svelte';
import Slider from '$lib/components/Slider.svelte';
import { attributionScoreScale } from '../../stores/scales';
import { import {
platformFilter, platformFilter,
actorNationFilter, actorNationFilter,
sourceFilter, sourceFilter,
sourceCategoryFilter, sourceCategoryFilter,
methodFilter, methodFilter,
selectAllFilters selectAllFilters,
attributionScoreFilter,
attributionScoreDef
} from '../../stores/filters'; } from '../../stores/filters';
$: console.log($attributionScoreFilter)
export let cases; export let cases;
function handleButtonClick() { function handleButtonClick() {
@ -40,6 +46,14 @@
</script> </script>
{#if cases} {#if cases}
<Slider value={$attributionScoreFilter}
label="Attribution Score"
min={attributionScoreDef[0]}
max={attributionScoreDef[1]}
showHandleLabels={false}
startColor={$attributionScoreScale(attributionScoreDef[0])}
stopColor={$attributionScoreScale(attributionScoreDef[1])}
on:changed={(e) => $attributionScoreFilter = e.detail} />
<Dropdown <Dropdown
items={addCount($actorNationFilter, 'actor_nation', cases)} items={addCount($actorNationFilter, 'actor_nation', cases)}
label="Actor nation" label="Actor nation"

156
src/lib/components/Slider.svelte Обычный файл
Просмотреть файл

@ -0,0 +1,156 @@
<script>
// 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})`;
</script>
<div class="slider"
bind:clientWidth={sliderWidth}
style="--handle-width: {handleWidth}px;">
{#if (showLabel)}
<div class="label">
{label}
</div>
{/if}
<div class="slider-body" class:border={showBorder}>
<div class="slider-selected-range"
style="width: {sliderWidth - 2 * handleWidth}px;
margin-left: {1 * handleWidth}px;
opacity: {barOpacity};
background: {background};"></div>
<div class="slider-handle"
class:no-label={!showHandleLabels}
style="left: {(Math.abs(value[0] - value[1]) < 0.1) ? pos.left - 5 : pos.left}px;"
use:slidable
on:slide={(e) => handleSlide(e, 'left')}
on:slideend={(e) => handleSlideEnd(e, 'left')}>
<span class="disable-select">{showHandleLabels ? Math.round(scale.invert(pos.left), 0) : ''}</span>
</div>
<div class="slider-handle"
class:no-label={!showHandleLabels}
style="left: {(Math.abs(value[0] - value[1]) < 0.1) ? pos.right + 5 : pos.right}px;"
use:slidable
on:slide={(e) => handleSlide(e, 'right')}
on:slideend={(e) => handleSlideEnd(e, 'right')}>
<span class="disable-select">{showHandleLabels ? Math.round(scale.invert(pos.right), 0) : ''}</span>
</div>
</div>
</div>
<style>
.slider {
display: flex;
flex-direction: column;
font-family: var(--font-02);
width: 200px;
max-width: 200px;
margin: 0.3rem 0.3rem 0 0.3rem;
position: relative;
pointer-events: all;
}
.label {
margin: 0 0 0.1rem 0;
font-size: 0.7rem;
color: var(--usa-blue);
}
.slider-body {
display: flex;
align-items: center;
width: 100%;
height: 1.7rem;
padding: 0.1rem 0;
font-size: 0.7rem;
background-color: var(--bg);
border: none;
position: relative;
}
.border {
border: 2px solid var(--usa-blue);
border-radius: 3px;
}
.slider-selected-range {
height: 8px;
border: none;
border-radius: 2px;
position: absolute;
z-index: 100;
}
.slider-handle {
width: var(--handle-width);
height: var(--handle-width);
border: 2px solid var(--usa-blue);
border-radius: 50%;
background-color: var(--bg);
cursor: pointer;
position: absolute;
z-index: 10000;
}
.slider-handle > span {
width: 100%;
height: 100%;
font-size: 0.7rem;
text-align: center;
color: var(--usa-blue);
position: absolute;
}
</style>

11
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';

Просмотреть файл

@ -1,3 +1,5 @@
import { min, max } from 'd3-array';
// consistent sort function // consistent sort function
export const sortConsistently = (itemA, itemB, property, key) => { export const sortConsistently = (itemA, itemB, property, key) => {
let valueA = itemA[property]; let valueA = itemA[property];
@ -31,4 +33,16 @@ export const splitString = (s) => {
// check if there's overlap between array and filter // check if there's overlap between array and filter
export const haveOverlap = (filter, arr) => export const haveOverlap = (filter, arr) =>
filter.filter((d) => d.selected).map((d) => d.id).some((item) => arr.includes(item)); 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]);

25
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]));
};

Просмотреть файл

@ -7,14 +7,17 @@
import CaseTable from '$lib/components/CaseTable.svelte'; import CaseTable from '$lib/components/CaseTable.svelte';
import Timeline from '$lib/components/Timeline.svelte'; import Timeline from '$lib/components/Timeline.svelte';
import Controls from '$lib/components/Controls.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 { import {
platformFilter, platformFilter,
actorNationFilter, actorNationFilter,
sourceFilter, sourceFilter,
sourceCategoryFilter, sourceCategoryFilter,
methodFilter methodFilter,
attributionScoreFilter,
attributionScoreDef
} from '../stores/filters'; } from '../stores/filters';
let cases = []; let cases = [];
@ -27,6 +30,8 @@
d.platform = splitString(d.platform) d.platform = splitString(d.platform)
d.actor_nation = splitString(d.actor_nation) d.actor_nation = splitString(d.actor_nation)
d.methods = splitString(d.methods) d.methods = splitString(d.methods)
d.attribution_total_score = +d.attribution_total_score
d.attribution_date = new Date(d.attribution_date)
d.show = false d.show = false
}) })
@ -35,8 +40,9 @@
sourceFilter.init(cases, 'source') sourceFilter.init(cases, 'source')
sourceCategoryFilter.init(cases, 'source_category') sourceCategoryFilter.init(cases, 'source_category')
methodFilter.init(cases, 'methods') methodFilter.init(cases, 'methods')
$attributionScoreFilter = attributionScoreDef;
console.log(cases.map(d => d.methods)) //console.log(cases.map(d => d.attribution_date))
}); });
$: if (cases) { $: if (cases) {
@ -47,9 +53,21 @@
&& haveOverlap($sourceFilter, d.source) && haveOverlap($sourceFilter, d.source)
&& haveOverlap($sourceCategoryFilter, d.source_category) && haveOverlap($sourceCategoryFilter, d.source_category)
&& haveOverlap($methodFilter, d.methods) && 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);
</script> </script>
<section class="section"> <section class="section">

6
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);