Этот коммит содержится в:
higsch 2020-10-09 08:50:01 +02:00
родитель a2fa2b81e7 61ffa72b92
Коммит e024f4579e
14 изменённых файлов: 58269 добавлений и 90 удалений

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,65 +41,72 @@
</script>
{#if (timePoints)}
<div class="controls">
<SearchText searchString={$textSearchFilter}
label="Search"
on:change={(e) => $textSearchFilter = e.detail}
on:reset={() => textSearchFilter.reset()} />
<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 items={addCount($disinformantNationFilter, 'disinformantNation', timePoints)}
label="Disinformant Nation"
superior
on:itemsAdded={(e) => disinformantNationFilter.select(e.detail)}
on:itemsRemoved={(e) => disinformantNationFilter.unselect(e.detail)} />
<Dropdown items={addCount($platformFilter, 'platforms', timePoints)}
label="Platform"
on:itemsAdded={(e) => platformFilter.select(e.detail)}
on:itemsRemoved={(e) => platformFilter.unselect(e.detail)} />
<Dropdown items={addCount($sourceFilter, 'sourceFilter', timePoints)}
label="Source"
hideOneHitWonders
superior
on:itemsAdded={(e) => sourceFilter.select(e.detail)}
on:itemsRemoved={(e) => sourceFilter.unselect(e.detail)} />
<Dropdown items={addCount($sourceCategoryFilter, 'sourceCategory', timePoints)}
label="Source Category"
on:itemsAdded={(e) => sourceCategoryFilter.select(e.detail)}
on:itemsRemoved={(e) => sourceCategoryFilter.unselect(e.detail)} />
<Dropdown items={addCount($methodFilter, 'methods', timePoints)}
label="Method"
superior
on:itemsAdded={(e) => methodFilter.select(e.detail)}
on:itemsRemoved={(e) => methodFilter.unselect(e.detail)} />
<Dropdown items={$contextData}
label="Context Dataset"
nameField="name"
on:itemsAdded={(e) => contextData.select(e.detail)}
on:itemsRemoved={(e) => contextData.unselect(e.detail)} />
<button class="reset-filters"
on:click={() => handleButtonClick()}>
Reset
</button>
<div class="controls-inner-wrapper">
<div class="controls">
<SearchText searchString={$textSearchFilter}
label="Search"
on:change={(e) => $textSearchFilter = e.detail}
on:reset={() => textSearchFilter.reset()} />
<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 items={addCount($disinformantNationFilter, 'disinformantNation', timePoints)}
label="Disinformant Nation"
superior
on:itemsAdded={(e) => disinformantNationFilter.select(e.detail)}
on:itemsRemoved={(e) => disinformantNationFilter.unselect(e.detail)} />
<Dropdown items={addCount($platformFilter, 'platforms', timePoints)}
label="Platform"
on:itemsAdded={(e) => platformFilter.select(e.detail)}
on:itemsRemoved={(e) => platformFilter.unselect(e.detail)} />
<Dropdown items={addCount($sourceFilter, 'sourceFilter', timePoints)}
label="Source"
hideOneHitWonders
superior
on:itemsAdded={(e) => sourceFilter.select(e.detail)}
on:itemsRemoved={(e) => sourceFilter.unselect(e.detail)} />
<Dropdown items={addCount($sourceCategoryFilter, 'sourceCategory', timePoints)}
label="Source Category"
on:itemsAdded={(e) => sourceCategoryFilter.select(e.detail)}
on:itemsRemoved={(e) => sourceCategoryFilter.unselect(e.detail)} />
<Dropdown items={addCount($methodFilter, 'methods', timePoints)}
label="Method"
superior
on:itemsAdded={(e) => methodFilter.select(e.detail)}
on:itemsRemoved={(e) => methodFilter.unselect(e.detail)} />
<Dropdown items={$contextData}
label="Context Dataset"
nameField="name"
on:itemsAdded={(e) => contextData.select(e.detail)}
on:itemsRemoved={(e) => contextData.unselect(e.detail)} />
<button class="reset-filters"
on:click={() => handleButtonClick()}>
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,
@ -115,31 +116,35 @@
style="width: {tWidth}px;
height: {Math.abs(tHeight - Math.abs(contentTop))}px;
position: absolute;
top: {$tooltip.tp.rSmiTot + 5}px;"></div>
top: {$tooltip.tp.rSmiTot + 5}px;
border: 1px solid black;"></div>
<div class="content"
bind:this={elem}
bind:clientHeight={tHeight}
style="top: {contentTop}px; margin: 0px {$tooltip.tp.rSmiTot / 2 + offset.left}px;">
style="top: {contentTop}px; margin: 0px {$tooltip.tp.rSmiTot / 3 + offset.left}px;">
<div class="scroll-wrapper"
bind:this={scrollWrapper}>
<div class="title">
<div class="title-dates">
<p class="no-break">{attributionTf($tooltip.tp.attributionDate)} | {@html highlight($tooltip.tp.disinformantAttribution)}</p>
<p>Active:
{#if ($tooltip.tp.startDate && !$tooltip.tp.endDate)}
{activityTf($tooltip.tp.startDate)} <span class="small">(approx.)</span>
{:else if (!$tooltip.tp.startDate && $tooltip.tp.endDate)}
{activityTf($tooltip.tp.endDate)} <span class="small">(approx.)</span>
{:else if ($tooltip.tp.startDate && $tooltip.tp.endDate)}
{#if (activityTf($tooltip.tp.startDate) === activityTf($tooltip.tp.endDate))}
{activityTf($tooltip.tp.startDate)}
<div class="title-top">
<div class="title-dates">
<p class="no-break">{attributionTf($tooltip.tp.attributionDate)} | {@html highlight($tooltip.tp.disinformantAttribution)}</p>
<p>Active:
{#if ($tooltip.tp.startDate && !$tooltip.tp.endDate)}
{activityTf($tooltip.tp.startDate)} <span class="small">(approx.)</span>
{:else if (!$tooltip.tp.startDate && $tooltip.tp.endDate)}
{activityTf($tooltip.tp.endDate)} <span class="small">(approx.)</span>
{:else if ($tooltip.tp.startDate && $tooltip.tp.endDate)}
{#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}
{activityTf($tooltip.tp.startDate)} to {activityTf($tooltip.tp.endDate)}
unspecified
{/if}
{:else}
unspecified
{/if}
</p>
</p>
</div>
<Share text="" caseId={$tooltip.tp.id} mode="tooltip" />
</div>
<h2>{@html highlight($tooltip.tp.shortTitle)}</h2>
<div class="score-bars">
@ -288,6 +293,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 = 'http://localhost:5000';//'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)
};
};