Этот коммит содержится в:
higsch 2020-10-08 23:54:26 +02:00
родитель 16e2f3877b
Коммит 562414925e
10 изменённых файлов: 274 добавлений и 81 удалений

9
package-lock.json сгенерированный
Просмотреть файл

@ -5393,6 +5393,15 @@
"integrity": "sha512-OX/IBVUJSFo1rnznXdwf9rv6LReJ3qQ0PwRjj76vfUWyTfbHbR9OXqJBnUrpjyis2dwYcbT2Zm1DFjOOF1ZbbQ==", "integrity": "sha512-OX/IBVUJSFo1rnznXdwf9rv6LReJ3qQ0PwRjj76vfUWyTfbHbR9OXqJBnUrpjyis2dwYcbT2Zm1DFjOOF1ZbbQ==",
"dev": true "dev": true
}, },
"svelte-awesome": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/svelte-awesome/-/svelte-awesome-2.3.0.tgz",
"integrity": "sha512-xXU3XYJmr5PK9e3pCpW6feU/tNpAhERWDrgDxkC0DU6LNum02zzc00I17bmUWTSRdKLf+IFbA5hWvCm1V8g+/A==",
"dev": true,
"requires": {
"svelte": "^3.15.0"
}
},
"svg-path-properties": { "svg-path-properties": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/svg-path-properties/-/svg-path-properties-0.2.2.tgz", "resolved": "https://registry.npmjs.org/svg-path-properties/-/svg-path-properties-0.2.2.tgz",

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

@ -27,6 +27,7 @@
"rollup-plugin-svelte": "^5.0.3", "rollup-plugin-svelte": "^5.0.3",
"rollup-plugin-terser": "^7.0.0", "rollup-plugin-terser": "^7.0.0",
"svelte": "^3.0.0", "svelte": "^3.0.0",
"svelte-awesome": "^2.3.0",
"topojson": "^3.0.2" "topojson": "^3.0.2"
}, },
"dependencies": { "dependencies": {

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

@ -1,23 +1,10 @@
<script> <script>
// loads data for context datasets and holds the individual graphing components // loads data for context datasets and holds the individual graphing components
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import loadCoronaData from '../utils/loadCoronaData';
import { contextData, originalTimeDomain } from '../stores/filters'; import { contextData, originalTimeDomain } from '../stores/filters';
import { margin } from '../stores/dimensions'; import { margin } from '../stores/dimensions';
import CoronaChart from './CoronaChart.svelte'; import CoronaChart from './CoronaChart.svelte';
onMount(async () => {
$contextData = [
{
id: 'corona',
name: 'COVID-19 in the US',
source: 'The New York Times',
data: await loadCoronaData(),
selected: false
}
];
});
</script> </script>
<g class="background-chart"> <g class="background-chart">

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

@ -17,6 +17,7 @@
import Dropdown from './Dropdown.svelte'; import Dropdown from './Dropdown.svelte';
import Slider from './Slider.svelte'; import Slider from './Slider.svelte';
import SearchText from './SearchText.svelte'; import SearchText from './SearchText.svelte';
import Share from './Share.svelte';
export let timePoints; export let timePoints;
@ -40,65 +41,72 @@
</script> </script>
{#if (timePoints)} {#if (timePoints)}
<div class="controls"> <div class="controls-inner-wrapper">
<SearchText searchString={$textSearchFilter} <div class="controls">
label="Search" <SearchText searchString={$textSearchFilter}
on:change={(e) => $textSearchFilter = e.detail} label="Search"
on:reset={() => textSearchFilter.reset()} /> on:change={(e) => $textSearchFilter = e.detail}
<Slider value={$attributionScoreFilter} on:reset={() => textSearchFilter.reset()} />
label="Attribution Score" <Slider value={$attributionScoreFilter}
min={attributionScoreDef[0]} label="Attribution Score"
max={attributionScoreDef[1]} min={attributionScoreDef[0]}
showHandleLabels={false} max={attributionScoreDef[1]}
startColor={$attributionScoreScale(attributionScoreDef[0])} showHandleLabels={false}
stopColor={$attributionScoreScale(attributionScoreDef[1])} startColor={$attributionScoreScale(attributionScoreDef[0])}
on:changed={(e) => $attributionScoreFilter = e.detail} /> stopColor={$attributionScoreScale(attributionScoreDef[1])}
<Dropdown items={addCount($disinformantNationFilter, 'disinformantNation', timePoints)} on:changed={(e) => $attributionScoreFilter = e.detail} />
label="Disinformant Nation" <Dropdown items={addCount($disinformantNationFilter, 'disinformantNation', timePoints)}
superior label="Disinformant Nation"
on:itemsAdded={(e) => disinformantNationFilter.select(e.detail)} superior
on:itemsRemoved={(e) => disinformantNationFilter.unselect(e.detail)} /> on:itemsAdded={(e) => disinformantNationFilter.select(e.detail)}
<Dropdown items={addCount($platformFilter, 'platforms', timePoints)} on:itemsRemoved={(e) => disinformantNationFilter.unselect(e.detail)} />
label="Platform" <Dropdown items={addCount($platformFilter, 'platforms', timePoints)}
on:itemsAdded={(e) => platformFilter.select(e.detail)} label="Platform"
on:itemsRemoved={(e) => platformFilter.unselect(e.detail)} /> on:itemsAdded={(e) => platformFilter.select(e.detail)}
<Dropdown items={addCount($sourceFilter, 'sourceFilter', timePoints)} on:itemsRemoved={(e) => platformFilter.unselect(e.detail)} />
label="Source" <Dropdown items={addCount($sourceFilter, 'sourceFilter', timePoints)}
hideOneHitWonders label="Source"
superior hideOneHitWonders
on:itemsAdded={(e) => sourceFilter.select(e.detail)} superior
on:itemsRemoved={(e) => sourceFilter.unselect(e.detail)} /> on:itemsAdded={(e) => sourceFilter.select(e.detail)}
<Dropdown items={addCount($sourceCategoryFilter, 'sourceCategory', timePoints)} on:itemsRemoved={(e) => sourceFilter.unselect(e.detail)} />
label="Source Category" <Dropdown items={addCount($sourceCategoryFilter, 'sourceCategory', timePoints)}
on:itemsAdded={(e) => sourceCategoryFilter.select(e.detail)} label="Source Category"
on:itemsRemoved={(e) => sourceCategoryFilter.unselect(e.detail)} /> on:itemsAdded={(e) => sourceCategoryFilter.select(e.detail)}
<Dropdown items={addCount($methodFilter, 'methods', timePoints)} on:itemsRemoved={(e) => sourceCategoryFilter.unselect(e.detail)} />
label="Method" <Dropdown items={addCount($methodFilter, 'methods', timePoints)}
superior label="Method"
on:itemsAdded={(e) => methodFilter.select(e.detail)} superior
on:itemsRemoved={(e) => methodFilter.unselect(e.detail)} /> on:itemsAdded={(e) => methodFilter.select(e.detail)}
<Dropdown items={$contextData} on:itemsRemoved={(e) => methodFilter.unselect(e.detail)} />
label="Context Dataset" <Dropdown items={$contextData}
nameField="name" label="Context Dataset"
on:itemsAdded={(e) => contextData.select(e.detail)} nameField="name"
on:itemsRemoved={(e) => contextData.unselect(e.detail)} /> on:itemsAdded={(e) => contextData.select(e.detail)}
<button class="reset-filters" on:itemsRemoved={(e) => contextData.unselect(e.detail)} />
on:click={() => handleButtonClick()}> <button class="reset-filters"
Reset on:click={() => handleButtonClick()}>
</button> Reset
</button>
</div>
<Share />
</div> </div>
{/if} {/if}
<style> <style>
.controls-inner-wrapper {
padding: 0.2rem;
border: none;
border-radius: 3px;
background-color: var(--transparentbg);
}
.controls { .controls {
display: grid; display: grid;
grid-auto-flow: column; grid-auto-flow: column;
grid-template-rows: repeat(9, 1fr); grid-template-rows: repeat(9, 1fr);
grid-gap: 0.3rem; grid-gap: 0.3rem;
padding: 0.5rem; margin-bottom: 0.7rem;
border: none;
border-radius: 3px;
background-color: var(--transparentbg);
} }
@media (min-width: 460px) { @media (min-width: 460px) {

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

@ -19,6 +19,7 @@
import EventTooltipCross from './EventTooltipCross.svelte'; import EventTooltipCross from './EventTooltipCross.svelte';
import ScoreBar from './ScoreBar.svelte'; import ScoreBar from './ScoreBar.svelte';
import ScoreQuestions from './ScoreQuestions.svelte'; import ScoreQuestions from './ScoreQuestions.svelte';
import Share from './Share.svelte';
const offset = { const offset = {
top: 10, top: 10,
@ -123,23 +124,26 @@
<div class="scroll-wrapper" <div class="scroll-wrapper"
bind:this={scrollWrapper}> bind:this={scrollWrapper}>
<div class="title"> <div class="title">
<div class="title-dates"> <div class="title-top">
<p class="no-break">{attributionTf($tooltip.tp.attributionDate)} | {@html highlight($tooltip.tp.disinformantAttribution)}</p> <div class="title-dates">
<p>Active: <p class="no-break">{attributionTf($tooltip.tp.attributionDate)} | {@html highlight($tooltip.tp.disinformantAttribution)}</p>
{#if ($tooltip.tp.startDate && !$tooltip.tp.endDate)} <p>Active:
{activityTf($tooltip.tp.startDate)} <span class="small">(approx.)</span> {#if ($tooltip.tp.startDate && !$tooltip.tp.endDate)}
{:else if (!$tooltip.tp.startDate && $tooltip.tp.endDate)} {activityTf($tooltip.tp.startDate)} <span class="small">(approx.)</span>
{activityTf($tooltip.tp.endDate)} <span class="small">(approx.)</span> {:else if (!$tooltip.tp.startDate && $tooltip.tp.endDate)}
{:else if ($tooltip.tp.startDate && $tooltip.tp.endDate)} {activityTf($tooltip.tp.endDate)} <span class="small">(approx.)</span>
{#if (activityTf($tooltip.tp.startDate) === activityTf($tooltip.tp.endDate))} {:else if ($tooltip.tp.startDate && $tooltip.tp.endDate)}
{activityTf($tooltip.tp.startDate)} {#if (activityTf($tooltip.tp.startDate) === activityTf($tooltip.tp.endDate))}
{activityTf($tooltip.tp.startDate)}
{:else}
{activityTf($tooltip.tp.startDate)} to {activityTf($tooltip.tp.endDate)}
{/if}
{:else} {:else}
{activityTf($tooltip.tp.startDate)} to {activityTf($tooltip.tp.endDate)} unspecified
{/if} {/if}
{:else} </p>
unspecified </div>
{/if} <Share text="" caseId={$tooltip.tp.id} mode="tooltip" />
</p>
</div> </div>
<h2>{@html highlight($tooltip.tp.shortTitle)}</h2> <h2>{@html highlight($tooltip.tp.shortTitle)}</h2>
<div class="score-bars"> <div class="score-bars">
@ -288,6 +292,16 @@
position: relative; position: relative;
} }
.title-top {
display: flex;
align-items: flex-start;
width: 100%;
}
.title-dates {
flex: 1;
}
.title-dates p { .title-dates p {
color: var(--text-black); color: var(--text-black);
font-size: 0.7rem; font-size: 0.7rem;

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

@ -0,0 +1,83 @@
<script>
import {
disinformantNationFilter,
platformFilter,
methodFilter,
sourceFilter,
sourceCategoryFilter,
attributionScoreFilter,
attributionScoreDef,
textSearchFilter,
originalTimeDomain,
contextData } from '../stores/filters';
import { urlFromFilters } from '../utils/share';
import Icon from 'svelte-awesome';
import { twitter, clipboard } from 'svelte-awesome/icons';
export let text = 'Share this view.';
export let caseId = '';
export let mode = 'standard';
async function copyToClipBoard() {
await navigator.clipboard.writeText(url);
const previousText = text;
text = 'Copied!';
setTimeout(() => text = previousText, 3000);
}
$: url = urlFromFilters(
$disinformantNationFilter,
$platformFilter,
$methodFilter,
$sourceFilter,
$sourceCategoryFilter,
$attributionScoreFilter,
$textSearchFilter,
$contextData,
caseId);
</script>
<div class="share">
<p class:gray={mode === 'tooltip'}>{text}</p>
<a class="twitter-share-button"
class:gray={mode === 'tooltip'}
href="https://twitter.com/intent/tweet?url={url.replace('#', '%23')}"
data-size="large"
target="_blank">
<Icon data={twitter}/>
</a>
<span class="pseudolink"
class:gray={mode === 'tooltip'}
on:click={copyToClipBoard}>
<Icon data={clipboard}/>
</span>
</div>
<style>
.share {
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-02);
font-size: 0.7rem;
pointer-events: all;
}
p {
color: var(--usa-blue);
}
a {
margin: 0 0.4rem;
}
.gray {
color: var(--text-darkgray);
transition: all 200ms ease;
}
.gray:hover {
color: var(--text-black);
}
</style>

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

@ -6,6 +6,7 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import loadData from '../utils/loadData'; import loadData from '../utils/loadData';
import loadMapData from '../utils/loadMapData'; import loadMapData from '../utils/loadMapData';
import loadCoronaData from '../utils/loadCoronaData';
import { setScales } from '../utils/scales'; import { setScales } from '../utils/scales';
import { import {
width, width,
@ -30,8 +31,10 @@
attributionScoreFilter, attributionScoreFilter,
attributionScoreDef, attributionScoreDef,
textSearchFilter, textSearchFilter,
originalTimeDomain } from '../stores/filters'; originalTimeDomain,
import { haveOverlap, withinRange, includesTextSearch, preloadImages } from '../utils/misc'; contextData,
caseIdFilter } from '../stores/filters';
import { haveOverlap, withinRange, includesTextSearch, isCaseId, preloadImages } from '../utils/misc';
import { selected } from '../stores/eventSelections'; import { selected } from '../stores/eventSelections';
import { drawWrapper } from '../stores/elements'; import { drawWrapper } from '../stores/elements';
@ -46,6 +49,7 @@
forceCollide, forceCollide,
timeFormat } from 'd3'; timeFormat } from 'd3';
import { sortConsistently } from '../utils/misc'; import { sortConsistently } from '../utils/misc';
import { parseUrl } from '../utils/share';
import ToTop from './ToTop.svelte'; import ToTop from './ToTop.svelte';
import TopVisualContent from './TopVisualContent.svelte'; import TopVisualContent from './TopVisualContent.svelte';
@ -85,7 +89,33 @@
sourceCategoryFilter.init(data, 'sourceCategory'); sourceCategoryFilter.init(data, 'sourceCategory');
$attributionScoreFilter = attributionScoreDef; $attributionScoreFilter = attributionScoreDef;
// get context datasets
$contextData = [
{
id: 'corona',
name: 'COVID-19 in the US',
source: 'The New York Times',
data: await loadCoronaData(),
selected: false
}
];
preloadImages(data); preloadImages(data);
// apply filters from URL
if (window.location.hash.length > 1) {
const urlFilters = parseUrl(window.location.hash);
disinformantNationFilter.applyBoolArray(urlFilters.disinformantNations);
platformFilter.applyBoolArray(urlFilters.platforms);
methodFilter.applyBoolArray(urlFilters.methods);
sourceFilter.applyBoolArray(urlFilters.sources);
sourceCategoryFilter.applyBoolArray(urlFilters.sourceCategories);
contextData.applyBoolArray(urlFilters.contextData);
$attributionScoreFilter = urlFilters.attributionScores;
$textSearchFilter = urlFilters.textSearch;
$caseIdFilter = urlFilters.caseId;
}
}); });
// set the scales // set the scales
@ -152,6 +182,7 @@
&& haveOverlap($sourceCategoryFilter, d.sourceCategory) && haveOverlap($sourceCategoryFilter, d.sourceCategory)
&& includesTextSearch($textSearchFilter, d.search) && includesTextSearch($textSearchFilter, d.search)
&& withinRange($attributionScoreFilter, d.attributionScore) && withinRange($attributionScoreFilter, d.attributionScore)
&& isCaseId($caseIdFilter, d.id)
})); }));
} }
</script> </script>

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

@ -18,6 +18,11 @@ function createInclusiveFilter() {
const select = (id) => update((f) => f.map((d) => ({...d, selected: [id].flat().includes(d.id) ? true : d.selected}))); const select = (id) => update((f) => f.map((d) => ({...d, selected: [id].flat().includes(d.id) ? true : d.selected})));
const unselectAll = () => update((f) => f.map((d) => ({...d, selected: false}))); const unselectAll = () => update((f) => f.map((d) => ({...d, selected: false})));
const applyBoolArray = (arr) => {
const tmpArr = [...arr].reverse();
update((f) => f.reverse().map((d, i) => ({...d, selected: tmpArr[i] !== undefined ? tmpArr[i] : false})).reverse());
};
return { return {
subscribe, subscribe,
set: (value) => set(value), set: (value) => set(value),
@ -29,7 +34,8 @@ function createInclusiveFilter() {
}, },
selectAll: () => update((f) => f.map((d) => ({...d, selected: true}))), selectAll: () => update((f) => f.map((d) => ({...d, selected: true}))),
unselect: (id) => update((f) => f.map((d) => ({...d, selected: [id].flat().includes(d.id) ? false : d.selected}))), unselect: (id) => update((f) => f.map((d) => ({...d, selected: [id].flat().includes(d.id) ? false : d.selected}))),
unselectAll unselectAll,
applyBoolArray
}; };
} }
@ -73,6 +79,7 @@ export const selectAllFilters = (disinformantNation = true) => {
sourceCategoryFilter.selectAll(); sourceCategoryFilter.selectAll();
attributionScoreFilter.set(attributionScoreDef); attributionScoreFilter.set(attributionScoreDef);
textSearchFilter.reset(); textSearchFilter.reset();
caseIdFilter.set(undefined);
}; };
export const textSearchFilter = createTextSearchFilter(); export const textSearchFilter = createTextSearchFilter();
@ -81,3 +88,5 @@ export const contextData = createInclusiveFilter();
export const brushed = writable(false); export const brushed = writable(false);
export const originalTimeDomain = writable(null); export const originalTimeDomain = writable(null);
export const caseIdFilter = writable();

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

@ -44,6 +44,9 @@ export const withinRange = (arr, num) => num >= arr[0] && num <= arr[1];
// check, if a search string (filter) is included in a string // check, if a search string (filter) is included in a string
export const includesTextSearch = (filter, s) => s.indexOf(filter.toUpperCase()) > -1; export const includesTextSearch = (filter, s) => s.indexOf(filter.toUpperCase()) > -1;
// check if case id filter is set and if id is matching
export const isCaseId = (filter, id) => filter === undefined ? true : (filter === id);
// extract filter items from data // extract filter items from data
export const extractFilterCategories = (data, name) => export const extractFilterCategories = (data, name) =>
uniq(data.map((d) => d[name]).flat()); uniq(data.map((d) => d[name]).flat());

48
src/utils/share.js Обычный файл
Просмотреть файл

@ -0,0 +1,48 @@
export const baseUrl = 'https://interference2020.org';
export const urlFromFilters = (disinformantNations,
platforms,
methods,
sources,
sourceCategories,
attributionScores,
textSearch,
contextData,
caseId = '') => {
const params = {
ts: textSearch,
as: [attributionScores[0], attributionScores[1]].join(';'),
f: filtersToHex([disinformantNations, platforms, methods, sources, sourceCategories, contextData]),
id: caseId
};
return `${baseUrl}/#${params.f}-${params.id}-${params.ts}-${params.as}`;
};
export const filtersToHex = (arr) => {
const hex = arr.map((d) => binaryToHex(d.map((d) => +d.selected).join(''))).join('-');
return hex;
};
export const binaryToHex = (binary) => parseInt(binary , 2).toString(16).toLowerCase();
export const hexToBinary = (hex) => parseInt(hex, 16).toString(2);
export const binaryToBool = (binary) => binary.split('').map((d) => d === '0' ? false : true);
export const parseUrl = (hash) => {
const s = hash.substring(1);
const [ disinformantNations, platforms, methods, sources, sourceCategories, contextData, caseId, textSearch, attributionScores] = s.split('-');
return {
disinformantNations: binaryToBool(hexToBinary(disinformantNations)),
platforms: binaryToBool(hexToBinary(platforms)),
methods: binaryToBool(hexToBinary(methods)),
sources: binaryToBool(hexToBinary(sources)),
sourceCategories: binaryToBool(hexToBinary(sourceCategories)),
contextData: binaryToBool(hexToBinary(contextData)),
caseId: caseId === '' ? undefined : +caseId,
textSearch,
attributionScores: attributionScores.split(';').map((d) => +d)
};
};