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>
|
<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
Обычный файл
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
|
// 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
Обычный файл
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
Обычный файл
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