Этот коммит содержится в:
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,6 +41,7 @@
</script> </script>
{#if (timePoints)} {#if (timePoints)}
<div class="controls-inner-wrapper">
<div class="controls"> <div class="controls">
<SearchText searchString={$textSearchFilter} <SearchText searchString={$textSearchFilter}
label="Search" label="Search"
@ -87,18 +89,24 @@
Reset Reset
</button> </button>
</div> </div>
<Share />
</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,14 +116,16 @@
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-top">
<div class="title-dates"> <div class="title-dates">
<p class="no-break">{attributionTf($tooltip.tp.attributionDate)} | {@html highlight($tooltip.tp.disinformantAttribution)}</p> <p class="no-break">{attributionTf($tooltip.tp.attributionDate)} | {@html highlight($tooltip.tp.disinformantAttribution)}</p>
<p>Active: <p>Active:
@ -141,6 +144,8 @@
{/if} {/if}
</p> </p>
</div> </div>
<Share text="" caseId={$tooltip.tp.id} mode="tooltip" />
</div>
<h2>{@html highlight($tooltip.tp.shortTitle)}</h2> <h2>{@html highlight($tooltip.tp.shortTitle)}</h2>
<div class="score-bars"> <div class="score-bars">
<div class="score-bar-wrapper"> <div class="score-bar-wrapper">
@ -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)
};
};