Slider filter
Этот коммит содержится в:
родитель
8ca51e21ea
Коммит
22052a9151
51
src/actions/slidable.js
Обычный файл
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>
|
||||
import Dropdown from '$lib/components/Dropdown.svelte';
|
||||
import Slider from '$lib/components/Slider.svelte';
|
||||
import { attributionScoreScale } from '../../stores/scales';
|
||||
import {
|
||||
platformFilter,
|
||||
actorNationFilter,
|
||||
sourceFilter,
|
||||
sourceCategoryFilter,
|
||||
methodFilter,
|
||||
selectAllFilters
|
||||
selectAllFilters,
|
||||
attributionScoreFilter,
|
||||
attributionScoreDef
|
||||
} from '../../stores/filters';
|
||||
|
||||
$: console.log($attributionScoreFilter)
|
||||
|
||||
export let cases;
|
||||
|
||||
function handleButtonClick() {
|
||||
@ -40,6 +46,14 @@
|
||||
</script>
|
||||
|
||||
{#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
|
||||
items={addCount($actorNationFilter, 'actor_nation', cases)}
|
||||
label="Actor nation"
|
||||
|
||||
156
src/lib/components/Slider.svelte
Обычный файл
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
Обычный файл
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
|
||||
export const sortConsistently = (itemA, itemB, property, key) => {
|
||||
let valueA = itemA[property];
|
||||
@ -32,3 +34,15 @@ 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));
|
||||
|
||||
// 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
Обычный файл
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 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);
|
||||
|
||||
</script>
|
||||
|
||||
<section class="section">
|
||||
|
||||
6
src/stores/scales.js
Обычный файл
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);
|
||||
Загрузка…
x
Ссылка в новой задаче
Block a user