diff --git a/package-lock.json b/package-lock.json index fe41bc2..60f92d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5393,6 +5393,15 @@ "integrity": "sha512-OX/IBVUJSFo1rnznXdwf9rv6LReJ3qQ0PwRjj76vfUWyTfbHbR9OXqJBnUrpjyis2dwYcbT2Zm1DFjOOF1ZbbQ==", "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": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/svg-path-properties/-/svg-path-properties-0.2.2.tgz", diff --git a/package.json b/package.json index 40226ad..b30a725 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "rollup-plugin-svelte": "^5.0.3", "rollup-plugin-terser": "^7.0.0", "svelte": "^3.0.0", + "svelte-awesome": "^2.3.0", "topojson": "^3.0.2" }, "dependencies": { diff --git a/src/components/BackgroundChart.svelte b/src/components/BackgroundChart.svelte index 08dce41..9d3d949 100644 --- a/src/components/BackgroundChart.svelte +++ b/src/components/BackgroundChart.svelte @@ -1,23 +1,10 @@ diff --git a/src/components/Controls.svelte b/src/components/Controls.svelte index 7d68b0a..186787c 100644 --- a/src/components/Controls.svelte +++ b/src/components/Controls.svelte @@ -17,6 +17,7 @@ import Dropdown from './Dropdown.svelte'; import Slider from './Slider.svelte'; import SearchText from './SearchText.svelte'; + import Share from './Share.svelte'; export let timePoints; @@ -40,65 +41,72 @@ {#if (timePoints)} -
- $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)} /> - +
+
+ $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) + }; +};