+
+ $textSearchFilter = e.detail}
+ on:reset={() => textSearchFilter.reset()} />
+ $attributionScoreFilter = e.detail} />
+ disinformantNationFilter.select(e.detail)}
+ on:itemsRemoved={(e) => disinformantNationFilter.unselect(e.detail)} />
+ platformFilter.select(e.detail)}
+ on:itemsRemoved={(e) => platformFilter.unselect(e.detail)} />
+ sourceFilter.select(e.detail)}
+ on:itemsRemoved={(e) => sourceFilter.unselect(e.detail)} />
+ sourceCategoryFilter.select(e.detail)}
+ on:itemsRemoved={(e) => sourceCategoryFilter.unselect(e.detail)} />
+ methodFilter.select(e.detail)}
+ on:itemsRemoved={(e) => methodFilter.unselect(e.detail)} />
+ contextData.select(e.detail)}
+ on:itemsRemoved={(e) => contextData.unselect(e.detail)} />
+
+
+
{/if}
diff --git a/src/components/Visualization.svelte b/src/components/Visualization.svelte
index 2778cb1..7ee00b8 100644
--- a/src/components/Visualization.svelte
+++ b/src/components/Visualization.svelte
@@ -6,6 +6,7 @@
import { onMount } from 'svelte';
import loadData from '../utils/loadData';
import loadMapData from '../utils/loadMapData';
+ import loadCoronaData from '../utils/loadCoronaData';
import { setScales } from '../utils/scales';
import {
width,
@@ -30,8 +31,10 @@
attributionScoreFilter,
attributionScoreDef,
textSearchFilter,
- originalTimeDomain } from '../stores/filters';
- import { haveOverlap, withinRange, includesTextSearch, preloadImages } from '../utils/misc';
+ originalTimeDomain,
+ contextData,
+ caseIdFilter } from '../stores/filters';
+ import { haveOverlap, withinRange, includesTextSearch, isCaseId, preloadImages } from '../utils/misc';
import { selected } from '../stores/eventSelections';
import { drawWrapper } from '../stores/elements';
@@ -46,6 +49,7 @@
forceCollide,
timeFormat } from 'd3';
import { sortConsistently } from '../utils/misc';
+ import { parseUrl } from '../utils/share';
import ToTop from './ToTop.svelte';
import TopVisualContent from './TopVisualContent.svelte';
@@ -85,7 +89,33 @@
sourceCategoryFilter.init(data, 'sourceCategory');
$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);
+
+ // 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
@@ -152,6 +182,7 @@
&& haveOverlap($sourceCategoryFilter, d.sourceCategory)
&& includesTextSearch($textSearchFilter, d.search)
&& withinRange($attributionScoreFilter, d.attributionScore)
+ && isCaseId($caseIdFilter, d.id)
}));
}
diff --git a/src/stores/filters.js b/src/stores/filters.js
index 003a39d..d244a07 100644
--- a/src/stores/filters.js
+++ b/src/stores/filters.js
@@ -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 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 {
subscribe,
set: (value) => set(value),
@@ -29,7 +34,8 @@ function createInclusiveFilter() {
},
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}))),
- unselectAll
+ unselectAll,
+ applyBoolArray
};
}
@@ -73,6 +79,7 @@ export const selectAllFilters = (disinformantNation = true) => {
sourceCategoryFilter.selectAll();
attributionScoreFilter.set(attributionScoreDef);
textSearchFilter.reset();
+ caseIdFilter.set(undefined);
};
export const textSearchFilter = createTextSearchFilter();
@@ -81,3 +88,5 @@ export const contextData = createInclusiveFilter();
export const brushed = writable(false);
export const originalTimeDomain = writable(null);
+
+export const caseIdFilter = writable();
diff --git a/src/utils/misc.js b/src/utils/misc.js
index 8e948ee..dc7b8a0 100644
--- a/src/utils/misc.js
+++ b/src/utils/misc.js
@@ -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
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
export const extractFilterCategories = (data, name) =>
uniq(data.map((d) => d[name]).flat());
diff --git a/src/utils/share.js b/src/utils/share.js
new file mode 100644
index 0000000..fbfbf57
--- /dev/null
+++ b/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)
+ };
+};