Этот коммит содержится в:
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==", "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,
@ -115,31 +116,35 @@
style="width: {tWidth}px; style="width: {tWidth}px;
height: {Math.abs(tHeight - Math.abs(contentTop))}px; height: {Math.abs(tHeight - Math.abs(contentTop))}px;
position: absolute; position: absolute;
top: {$tooltip.tp.rSmiTot + 5}px;"></div> top: {$tooltip.tp.rSmiTot + 5}px;
border: 1px solid black;"></div>
<div class="content" <div class="content"
bind:this={elem} bind:this={elem}
bind:clientHeight={tHeight} 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" <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 +293,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 = '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)
};
};