473 строки
14 KiB
Svelte
473 строки
14 KiB
Svelte
<script>
|
|
// a case tooltip (event = case)
|
|
import { afterUpdate } from 'svelte';
|
|
import { width, panelHeight, controlsHeight } from '../stores/dimensions';
|
|
import { tooltip } from '../stores/eventSelections';
|
|
import { fade, slide } from 'svelte/transition';
|
|
import { timeFormat, format } from 'd3';
|
|
import { extractHostname } from '../utils/misc';
|
|
import {
|
|
platformFilter,
|
|
methodFilter,
|
|
sourceFilter,
|
|
sourceCategoryFilter,
|
|
textSearchFilter,
|
|
selectAllFilters} from '../stores/filters';
|
|
import { maxScores } from '../inputs/scores';
|
|
import { images } from '../inputs/dataPaths';
|
|
|
|
import EventTooltipCross from './EventTooltipCross.svelte';
|
|
import ScoreBar from './ScoreBar.svelte';
|
|
import ScoreQuestions from './ScoreQuestions.svelte';
|
|
import Share from './Share.svelte';
|
|
|
|
const offset = {
|
|
top: 10,
|
|
right: 10,
|
|
bottom: 10,
|
|
left: 10
|
|
};
|
|
const contentOffset = -20;
|
|
|
|
const attributionTf = timeFormat('%B %d, %Y');
|
|
const activityTf = timeFormat('%B %Y');
|
|
const commaFormat = format(',');
|
|
|
|
let elem;
|
|
let tWidth, tHeight;
|
|
let side;
|
|
let left, top, contentTop;
|
|
let scrollWrapper;
|
|
|
|
let scoreQuestionsExpanded = false;
|
|
|
|
function handleLiClick(type, item) {
|
|
selectAllFilters();
|
|
switch (type) {
|
|
case 'platform': platformFilter.selectOne(item); break;
|
|
case 'method': methodFilter.selectOne(item); break;
|
|
case 'source': sourceFilter.selectOne(item); break;
|
|
case 'sourceCategory': sourceCategoryFilter.selectOne(item); break;
|
|
}
|
|
}
|
|
|
|
function highlight(s) {
|
|
if (!$textSearchFilter || $textSearchFilter === '') return s;
|
|
return s.replace(new RegExp($textSearchFilter.toLowerCase().split(' or ').join('|'), 'gi'), (match) => `<span class="highlighted">${match}</span>`);
|
|
}
|
|
|
|
$: if (showTooltip) {
|
|
scoreQuestionsExpanded = false;
|
|
|
|
side = $width - $tooltip.tp.x < $width / 2 ? 'left' : 'right';
|
|
|
|
top = $tooltip.tp.fy - offset.top;
|
|
|
|
// adjust for the difference between mouse hover point and balloon center
|
|
const balloonPos = $tooltip.e.pageY;
|
|
|
|
// the regular tooltip offset to the balloon
|
|
contentTop = contentOffset;
|
|
|
|
// if the tooltip hits the lower page boundary
|
|
if (balloonPos + contentTop + tHeight - window.pageYOffset > window.innerHeight) {
|
|
// console.log('lower')
|
|
contentTop -= balloonPos + tHeight - window.pageYOffset - window.innerHeight;
|
|
}
|
|
|
|
// if the tooltip hits the upper page boundary
|
|
if (balloonPos + contentTop - window.pageYOffset < $controlsHeight) {
|
|
// console.log('upper')
|
|
contentTop -= balloonPos + contentTop - window.pageYOffset - $controlsHeight - 50;
|
|
}
|
|
|
|
// // if the tooltip hits the uper border of the SVG
|
|
if ($tooltip.tp.fy + contentTop < $controlsHeight) {
|
|
// console.log('border')
|
|
contentTop -= $tooltip.tp.fy + contentTop - $controlsHeight;
|
|
}
|
|
|
|
if (side === 'left') {
|
|
left = $tooltip.tp.x - tWidth + offset.left;
|
|
} else if (side === 'right') {
|
|
left = $tooltip.tp.x - offset.left;
|
|
}
|
|
|
|
if (scrollWrapper) scrollWrapper.scrollTo(0, 0);
|
|
}
|
|
|
|
$: showTooltip = ($tooltip && $tooltip.tp && $tooltip.tp.show);
|
|
</script>
|
|
|
|
{#if (showTooltip)}
|
|
<div class="tooltip"
|
|
style="left: {left}px; top: {top}px;"
|
|
bind:clientWidth={tWidth}
|
|
on:click|stopPropagation
|
|
on:mouseover|stopPropagation
|
|
transition:fade={{duration: 200}}>
|
|
<EventTooltipCross {tWidth} {offset} {side} />
|
|
<div class="mouse-catcher"
|
|
style="width: {tWidth}px;
|
|
height: {Math.max(10, Math.abs(contentTop) - $tooltip.tp.rSmiTot + 25)}px;
|
|
position: absolute;
|
|
top: {contentTop - 10}px;"></div>
|
|
<div class="mouse-catcher"
|
|
style="width: {tWidth}px;
|
|
height: {Math.abs(tHeight - Math.abs(contentTop))}px;
|
|
position: absolute;
|
|
top: {$tooltip.tp.rSmiTot + 5}px;"></div>
|
|
<div class="content"
|
|
bind:this={elem}
|
|
bind:clientHeight={tHeight}
|
|
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-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}
|
|
unspecified
|
|
{/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">
|
|
<ScoreBar value={$tooltip.tp.attributionCredibilityScore} maxValue={maxScores.attributionCredibilityScore} />
|
|
<p>Credibility</p>
|
|
</div>
|
|
<div class="score-bar-wrapper">
|
|
<ScoreBar value={$tooltip.tp.attributionObjectivityScore} maxValue={maxScores.attributionObjectivityScore} />
|
|
<p>Objectivity</p>
|
|
</div>
|
|
<div class="score-bar-wrapper">
|
|
<ScoreBar value={$tooltip.tp.attributionEvidenceScore} maxValue={maxScores.attributionEvidenceScore} />
|
|
<p>Evidence</p>
|
|
</div>
|
|
<div class="score-bar-wrapper">
|
|
<ScoreBar value={$tooltip.tp.attributionTransparencyScore} maxValue={maxScores.attributionTransparencyScore} />
|
|
<p>Transparency</p>
|
|
</div>
|
|
<span class="score-info-icon disable-select" on:click|self={() => scoreQuestionsExpanded = !scoreQuestionsExpanded}>
|
|
{scoreQuestionsExpanded ? 'X' : '?'}
|
|
</span>
|
|
</div>
|
|
{#if (scoreQuestionsExpanded)}
|
|
<div class="store-questions-wrapper" transition:slide|local>
|
|
<ScoreQuestions timePoint={$tooltip.tp} />
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
<div class="smi">
|
|
<h3>Attribution impact</h3>
|
|
{#if ($tooltip.tp.smiPending)}
|
|
<p>pending</p>
|
|
{:else}
|
|
<ul>
|
|
<li>
|
|
<span class="smi-score facebook">{commaFormat($tooltip.tp.smiFacebook)}</span>
|
|
<span class="smi-label">Facebook</span>
|
|
</li>
|
|
<li>
|
|
<span class="smi-score twitter">{commaFormat($tooltip.tp.smiTwitter)}</span>
|
|
<span class="smi-label">Twitter</span>
|
|
</li>
|
|
<li>
|
|
<span class="smi-score reddit">{commaFormat($tooltip.tp.smiReddit)}</span>
|
|
<span class="smi-label">Reddit</span>
|
|
</li>
|
|
</ul>
|
|
{/if}
|
|
</div>
|
|
{#if ($tooltip.tp.imageUrl)}
|
|
<div class="image">
|
|
<img src="{images}{$tooltip.tp.caseHash}.jpg" alt={$tooltip.tp.shortTitle} />
|
|
<p>{$tooltip.tp.imageCredit}</p>
|
|
</div>
|
|
{/if}
|
|
<div class="description">
|
|
<h3>Description</h3>
|
|
<p>{@html highlight($tooltip.tp.shortDescription)}</p>
|
|
</div>
|
|
<div class="platforms">
|
|
<h3>Platforms</h3>
|
|
<ul>
|
|
{#each $tooltip.tp.platforms as platform (platform)}
|
|
<li class="card" on:click|self={() => handleLiClick('platform', platform)}>{@html highlight(platform)}</li>
|
|
{/each}
|
|
</ul>
|
|
</div>
|
|
<div class="methods">
|
|
<h3>Methods</h3>
|
|
<ul>
|
|
{#each $tooltip.tp.methods as method (method)}
|
|
<li class="card" on:click|self={() => handleLiClick('method', method)}>{@html highlight(method)}</li>
|
|
{/each}
|
|
</ul>
|
|
</div>
|
|
<div class="source">
|
|
<h3>Source{$tooltip.tp.source.length !== 1 ? 's' : ''}</h3>
|
|
<ul>
|
|
{#each $tooltip.tp.source as source, i (source)}
|
|
<li class="card" on:click|self={() => handleLiClick('source', $tooltip.tp.sourceFilter[i] ? $tooltip.tp.sourceFilter[i] : $tooltip.tp.sourceFilter.slice(-1)[0])}>
|
|
{#if ($tooltip.tp.sourceFilter[i] && $tooltip.tp.sourceFilter[i] !== source)}
|
|
{@html highlight(source)} / {@html highlight($tooltip.tp.sourceFilter[i])}
|
|
{:else if (!$tooltip.tp.sourceFilter[i] && $tooltip.tp.sourceFilter.slice(-1)[0] !== source)}
|
|
{@html highlight(source)} / {@html highlight($tooltip.tp.sourceFilter.slice(-1)[0])}
|
|
{:else if ($tooltip.tp.sourceFilter[i])}
|
|
{@html highlight($tooltip.tp.sourceFilter[i])}
|
|
{:else}
|
|
{@html highlight($tooltip.tp.sourceFilter.slice(-1)[0])}
|
|
{/if}
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</div>
|
|
<div class="source-category">
|
|
<h3>Source categor{$tooltip.tp.sourceCategory.length !== 1 ? 'ies' : 'y'}</h3>
|
|
<ul>
|
|
{#each $tooltip.tp.sourceCategory as cat (cat)}
|
|
<li class="card" on:click|self={() => handleLiClick('sourceCategory', cat)}>{@html highlight(cat)}</li>
|
|
{/each}
|
|
</ul>
|
|
</div>
|
|
<div class="link">
|
|
<h3>Link</h3>
|
|
<a href={$tooltip.tp.attributionUrl} target="_blank" class="no-float">{extractHostname($tooltip.tp.attributionUrl)}</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<style>
|
|
.tooltip {
|
|
width: 70%;
|
|
/* min-width: 550px; */
|
|
font-family: var(--font-02);
|
|
position: absolute;
|
|
z-index: 10000;
|
|
}
|
|
|
|
@media (min-width: 800px) {
|
|
.tooltip {
|
|
width: 21%;
|
|
min-width: 550px;
|
|
}
|
|
}
|
|
|
|
.content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
max-height: 60vh;
|
|
color: var(--text-black);
|
|
background-color: var(--bg);
|
|
pointer-events: all;
|
|
box-shadow: 0 1px 2px rgba(0,0,0,0.07),
|
|
0 2px 4px rgba(0,0,0,0.07),
|
|
0 4px 8px rgba(0,0,0,0.07),
|
|
0 8px 16px rgba(0,0,0,0.07),
|
|
0 16px 32px rgba(0,0,0,0.07),
|
|
0 32px 64px rgba(0,0,0,0.07);
|
|
position: absolute;
|
|
}
|
|
|
|
.scroll-wrapper {
|
|
height: 100%;
|
|
overflow-x: hidden;
|
|
overflow-y: scroll;
|
|
}
|
|
|
|
.scroll-wrapper .title {
|
|
padding: 1rem;
|
|
background-image: linear-gradient(var(--usa-lightlightred), var(--usa-lightred));
|
|
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;
|
|
}
|
|
|
|
.scroll-wrapper > div {
|
|
width: 100%;
|
|
padding: 0.5rem 1rem;
|
|
}
|
|
|
|
h2, h3 {
|
|
color: var(--text-black);
|
|
}
|
|
|
|
h2 {
|
|
margin: 1rem 0;
|
|
font-size: 1.1rem;
|
|
font-weight: bold;
|
|
}
|
|
|
|
h3 {
|
|
margin: 0 0 0.1rem 0;
|
|
font-size: 0.9rem;
|
|
font-weight: normal;
|
|
clear: both;
|
|
}
|
|
|
|
.score-bars {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.score-bar-wrapper {
|
|
flex: 1 1 0;
|
|
display: inline-block;
|
|
}
|
|
|
|
.score-bar-wrapper p {
|
|
font-size: 0.7rem;
|
|
}
|
|
|
|
.score-bars span.score-info-icon {
|
|
width: 1rem;
|
|
height: 1rem;
|
|
margin: 0;
|
|
padding: 0 auto 0.1rem auto;
|
|
font-size: 0.7rem;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
color: var(--usa-lightred);
|
|
border: 2px solid var(--text-darkgray);
|
|
border-radius: 2px;
|
|
background-color: var(--text-darkgray);
|
|
transition: all 400ms ease;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.score-bars span.score-info-icon:hover {
|
|
color: var(--text-darkgray);
|
|
background-color: var(--usa-lightred);
|
|
}
|
|
|
|
.store-questions-wrapper {
|
|
position: relative;
|
|
}
|
|
|
|
h3 {
|
|
margin: 0 0 0.1rem 0;
|
|
font-size: 0.9rem;
|
|
font-weight: normal;
|
|
color: var(--text-black);
|
|
clear: both;
|
|
}
|
|
|
|
p {
|
|
font-size: 0.8rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
ul {
|
|
display: inline-block;
|
|
list-style-type: none;
|
|
}
|
|
|
|
li.card, a {
|
|
float: left;
|
|
margin: 0.1rem 0.2rem 0.1rem 0;
|
|
padding: 0.1rem 0.3rem;
|
|
font-size: 0.8rem;
|
|
color: var(--text-black);
|
|
border: none;
|
|
border-radius: 0.2rem;
|
|
background-color: var(--usa-lightlightred);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
transition: background-color 200ms ease;
|
|
box-shadow: 0 1px 2px rgba(0,0,0,0.07),
|
|
0 2px 4px rgba(0,0,0,0.07);
|
|
}
|
|
|
|
.smi ul {
|
|
display: flex;
|
|
}
|
|
|
|
.smi li {
|
|
margin: 0.2rem 0.3rem 0.2rem 0;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.smi-score {
|
|
padding: 0 0.2rem;
|
|
border: none;
|
|
border-radius: 3px;
|
|
box-shadow: 0 1px 2px rgba(0,0,0,0.07),
|
|
0 2px 4px rgba(0,0,0,0.07);
|
|
}
|
|
|
|
.smi-label {
|
|
display: inline-block;
|
|
padding: 0 0.1rem;
|
|
border: none;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
a {
|
|
text-decoration: none;
|
|
}
|
|
|
|
a:hover, li.card:hover {
|
|
background-color: var(--usa-lightred);
|
|
}
|
|
|
|
.small {
|
|
font-size: 0.6rem;
|
|
}
|
|
|
|
.no-break {
|
|
word-break: keep-all;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.scroll-wrapper .image {
|
|
min-height: 1%;
|
|
width: 100%;
|
|
}
|
|
|
|
.image img {
|
|
width: 100%;
|
|
}
|
|
|
|
.image p {
|
|
width: 100%;
|
|
font-size: 0.5rem;
|
|
text-align: right;
|
|
}
|
|
|
|
.no-float {
|
|
float: none;
|
|
}
|
|
</style>
|