share buttons, share URLs
Этот коммит содержится в:
родитель
16e2f3877b
Коммит
562414925e
9
package-lock.json
сгенерированный
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,
|
||||||
@ -123,23 +124,26 @@
|
|||||||
<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 +292,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
Обычный файл
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
Обычный файл
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)
|
||||||
|
};
|
||||||
|
};
|
||||||
Загрузка…
x
Ссылка в новой задаче
Block a user