Этот коммит содержится в:
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==",
"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",

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

@ -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": {

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

@ -1,23 +1,10 @@
<script>
// loads data for context datasets and holds the individual graphing components
import { onMount } from 'svelte';
import loadCoronaData from '../utils/loadCoronaData';
import { contextData, originalTimeDomain } from '../stores/filters';
import { margin } from '../stores/dimensions';
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>
<g class="background-chart">

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

@ -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,6 +41,7 @@
</script>
{#if (timePoints)}
<div class="controls-inner-wrapper">
<div class="controls">
<SearchText searchString={$textSearchFilter}
label="Search"
@ -87,18 +89,24 @@
Reset
</button>
</div>
<Share />
</div>
{/if}
<style>
.controls-inner-wrapper {
padding: 0.2rem;
border: none;
border-radius: 3px;
background-color: var(--transparentbg);
}
.controls {
display: grid;
grid-auto-flow: column;
grid-template-rows: repeat(9, 1fr);
grid-gap: 0.3rem;
padding: 0.5rem;
border: none;
border-radius: 3px;
background-color: var(--transparentbg);
margin-bottom: 0.7rem;
}
@media (min-width: 460px) {

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

@ -19,6 +19,7 @@
import EventTooltipCross from './EventTooltipCross.svelte';
import ScoreBar from './ScoreBar.svelte';
import ScoreQuestions from './ScoreQuestions.svelte';
import Share from './Share.svelte';
const offset = {
top: 10,
@ -123,6 +124,7 @@
<div class="scroll-wrapper"
bind:this={scrollWrapper}>
<div class="title">
<div class="title-top">
<div class="title-dates">
<p class="no-break">{attributionTf($tooltip.tp.attributionDate)} | {@html highlight($tooltip.tp.disinformantAttribution)}</p>
<p>Active:
@ -141,6 +143,8 @@
{/if}
</p>
</div>
<Share text="" caseId={$tooltip.tp.id} mode="tooltip" />
</div>
<h2>{@html highlight($tooltip.tp.shortTitle)}</h2>
<div class="score-bars">
<div class="score-bar-wrapper">
@ -288,6 +292,16 @@
position: relative;
}
.title-top {
display: flex;
align-items: flex-start;
width: 100%;
}
.title-dates {
flex: 1;
}
.title-dates p {
color: var(--text-black);
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 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)
}));
}
</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 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();

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

@ -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());

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)
};
};