Этот коммит содержится в:
Maarten 2024-10-05 22:37:07 +02:00
родитель ed89c0f3f6
Коммит 5db9ecdb67
2 изменённых файлов: 227 добавлений и 173 удалений

Просмотреть файл

@ -1,6 +1,7 @@
<script> <script>
import { utcFormat } from 'd3-time-format'; import { utcFormat } from 'd3-time-format';
export let cases export let cases
console.log(cases.map(d => d.Attribution_ID))
</script> </script>
<div class="table-container"> <div class="table-container">

Просмотреть файл

@ -2,143 +2,171 @@
import copy from '../data/copy.json'; import copy from '../data/copy.json';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { csv } from 'd3-fetch'; import { csv } from 'd3-fetch';
import { max, extent } from 'd3-array'; import { max, extent } from 'd3-array';
import { base } from '$app/paths'; import { base } from '$app/paths';
import CaseCard from '$lib/components/CaseCard.svelte'; import CaseCard from '$lib/components/CaseCard.svelte';
import CaseTable from '$lib/components/CaseTable.svelte'; import CaseTable from '$lib/components/CaseTable.svelte';
import Timeline from '$lib/components/Timeline.svelte'; import Timeline from '$lib/components/Timeline.svelte';
import TimelineMobile from '$lib/components/TimelineMobile.svelte'; import TimelineMobile from '$lib/components/TimelineMobile.svelte';
import Controls from '$lib/components/Controls.svelte'; import Controls from '$lib/components/Controls.svelte';
import AnimatedFilterIcon from '$lib/components/AnimatedFilterIcon.svelte'; import AnimatedFilterIcon from '$lib/components/AnimatedFilterIcon.svelte';
import { splitString, haveOverlap, withinRange, includesTextSearch } from '$lib/utils/misc' import { splitString, haveOverlap, withinRange, includesTextSearch } from '$lib/utils/misc';
//import { setScales } from '$lib/utils/scales'; //import { setScales } from '$lib/utils/scales';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { parseUrl } from '$lib/utils/share'; import { parseUrl } from '$lib/utils/share';
import { import {
platformFilter, platformFilter,
actorNationFilter, actorNationFilter,
sourceFilter, sourceFilter,
sourceCategoryFilter, sourceCategoryFilter,
methodFilter, methodFilter,
attributionScoreFilter, attributionScoreFilter,
attributionScoreDef, attributionScoreDef,
textSearchFilter, textSearchFilter,
timeRangeFilter, timeRangeFilter,
fullTimeRange fullTimeRange
} from '../stores/filters'; } from '../stores/filters';
$: innerWidth = 0 $: innerWidth = 0;
$: isMobile = innerWidth < 520 $: isMobile = innerWidth < 520;
$: displayDataAs = isMobile ? "Cards" : "Table" $: displayDataAs = isMobile ? 'Cards' : 'Table';
let cases = []; let cases = [];
let events = []; let events = [];
let metrics = []; let metrics = [];
let maxAttribution = 0; let maxAttribution = 0;
onMount(async function () { onMount(async function () {
const response = await csv(`https://fiat-2024-processed-data.s3.us-west-2.amazonaws.com/Demo_Attribution_Data.csv`); const response = await csv(
`https://fiat-2024-processed-data.s3.us-west-2.amazonaws.com/Demo_Attribution_Data.csv`
);
//const response = await csv(`${base}/Demo_Attribution_Data.csv`); //const response = await csv(`${base}/Demo_Attribution_Data.csv`);
cases = response; cases = response;
cases = cases.filter(d => d.Attribution_ID != '') cases = cases.filter((d) => d.Attribution_ID != '');
cases.forEach(d => { cases.forEach((d) => {
d.platform = splitString(d.Platforms) d.platform = splitString(d.Platforms);
d.actor_nation = splitString(d.Actor_Nation) d.actor_nation = splitString(d.Actor_Nation);
d.source = splitString(d.Source) d.source = splitString(d.Source);
d.methods = splitString(d.Methods) d.methods = splitString(d.Methods);
d.attribution_total_score = +d.attribution_total_score d.attribution_total_score = +d.attribution_total_score;
d.attribution_date = new Date(d.Attribution_Date) d.attribution_date = new Date(d.Attribution_Date);
d.search = [d.Short_Description, d.Short_Title, d.platform, d.methods, d.Source, d.Source_Nation, d.Source_cCtegory].flat().join('__').toLowerCase() d.search = [
d.Short_Description,
d.Short_Title,
d.platform,
d.methods,
d.Source,
d.Source_Nation,
d.Source_Category
].flat()
.join('__')
.toLowerCase();
d.show = false d.show = false;
}) });
maxAttribution = max(cases.map(d => d.attribution_total_score)) maxAttribution = max(cases.map((d) => d.attribution_total_score));
platformFilter.init(cases, 'platform'); platformFilter.init(cases, 'platform');
actorNationFilter.init(cases, 'actor_nation'); actorNationFilter.init(cases, 'actor_nation');
sourceFilter.init(cases, 'source') sourceFilter.init(cases, 'source');
sourceCategoryFilter.init(cases, 'Source_Category') sourceCategoryFilter.init(cases, 'Source_Category');
methodFilter.init(cases, 'methods') methodFilter.init(cases, 'methods');
$attributionScoreFilter = attributionScoreDef; $attributionScoreFilter = attributionScoreDef;
$timeRangeFilter = extent(cases.map((d) => new Date(d.attribution_date))) $timeRangeFilter = extent(cases.map((d) => new Date(d.attribution_date)));
$fullTimeRange = extent(cases.map((d) => new Date(d.attribution_date))) $fullTimeRange = extent(cases.map((d) => new Date(d.attribution_date)));
const eventsResponse = await csv(`https://fiat-2024-processed-data.s3.us-west-2.amazonaws.com/Key_Events_List.csv`) const eventsResponse = await csv(
events = eventsResponse `https://fiat-2024-processed-data.s3.us-west-2.amazonaws.com/Key_Events_List.csv`
events.forEach(d => { );
d.date = new Date(d.Date) events = eventsResponse;
}) events.forEach((d) => {
d.date = new Date(d.Date);
});
const metricsResponse = await csv('https://fiat-2024-processed-data.s3.us-west-2.amazonaws.com/fiat_country_metrics.csv') const metricsResponse = await csv(
'https://fiat-2024-processed-data.s3.us-west-2.amazonaws.com/fiat_country_metrics.csv'
);
metrics = metricsResponse.map(d => { metrics = metricsResponse.map((d) => {
let obj = {} let obj = {};
obj.date = new Date(d.Date), (obj.date = new Date(d.Date)), (obj.posts = +d.Posts);
obj.posts = +d.Posts obj.country = d.Country;
obj.country = d.Country return obj;
return obj });
} metrics.sort((a, b) => {
) return a.date - b.date;
metrics.sort((a, b) =>{ });
return a.date - b.date
})
//console.log($page.url.searchParams.get('filters'))
if ($page.url.searchParams.has('filters')) {
const urlFilters = parseUrl($page.url.searchParams.get('filters'));
console.log(urlFilters)
actorNationFilter.applyBoolArray(urlFilters.actorNations); if ($page.url.searchParams.has('filters')) {
platformFilter.applyBoolArray(urlFilters.platforms); const urlFilters = parseUrl($page.url.searchParams.get('filters'));
methodFilter.applyBoolArray(urlFilters.methods);
sourceFilter.applyBoolArray(urlFilters.sources); actorNationFilter.applyBoolArray(urlFilters.actorNations);
sourceCategoryFilter.applyBoolArray(urlFilters.sourceCategories); platformFilter.applyBoolArray(urlFilters.platforms);
$attributionScoreFilter = urlFilters.attributionScores; methodFilter.applyBoolArray(urlFilters.methods);
$textSearchFilter = urlFilters.textSearch; sourceFilter.applyBoolArray(urlFilters.sources);
} sourceCategoryFilter.applyBoolArray(urlFilters.sourceCategories);
$attributionScoreFilter = urlFilters.attributionScores;
$textSearchFilter = urlFilters.textSearch;
}
}); });
$: if (cases) {
cases = cases.map(d => ({
...d,
show: haveOverlap($actorNationFilter, d.Actor_Nation)
&& haveOverlap($platformFilter, d.platform)
&& haveOverlap($sourceFilter, d.source)
&& haveOverlap($sourceCategoryFilter, d.Source_Category)
&& haveOverlap($methodFilter, d.methods)
&& withinRange($attributionScoreFilter, d.attribution_total_score)
&& withinRange($timeRangeFilter, d.attribution_date)
&& includesTextSearch($textSearchFilter, d.search)
}))
}
let width = 1200 let sortedCases = []
let margin = { $: if (cases) {
top: 30, cases = cases.map((d) => ({
right: 30, ...d,
bottom: 30, show:
left: 30 haveOverlap($actorNationFilter, d.Actor_Nation) &&
} haveOverlap($platformFilter, d.platform) &&
haveOverlap($sourceFilter, d.source) &&
// set the scales haveOverlap($sourceCategoryFilter, d.Source_Category) &&
//$: setScales(cases, width, margin); haveOverlap($methodFilter, d.methods) &&
withinRange($attributionScoreFilter, d.attribution_total_score) &&
let sidebarOpen = false; withinRange($timeRangeFilter, d.attribution_date) &&
includesTextSearch($textSearchFilter, d.search)
let toggleSidebar = function(){ }));
sidebarOpen = !sidebarOpen sortedCases = cases.sort((a, b) => a[selectedSorting.id] - b[selectedSorting.id])
sortedCases = sortedCases
} }
let width = 1200;
let margin = {
top: 30,
right: 30,
bottom: 30,
left: 30
};
// set the scales
//$: setScales(cases, width, margin);
let sidebarOpen = false;
let toggleSidebar = function () {
sidebarOpen = !sidebarOpen;
};
const sortOptions = [
{id: 'attribution_date', label: 'Attribution Date'},
{id: 'attribution_total_score', label: 'Attribution Score'},
{id: 'actor_nation', label: 'Actor Nation'},
{id: 'platform', label: 'Platform'},
{id: 'source', label: 'Source'},
{id: 'Source_Category', label: 'Source Category'},
]
let selectedSorting = {id: 'attribution_date', label: 'Attribution Date'}
</script> </script>
<svelte:window bind:innerWidth /> <svelte:window bind:innerWidth />
{#if isMobile} {#if isMobile}
<div class="filter-button"> <div class="filter-button">
<button on:click={() => toggleSidebar()}><AnimatedFilterIcon {sidebarOpen}></AnimatedFilterIcon></button> <button on:click={() => toggleSidebar()}
</div> ><AnimatedFilterIcon {sidebarOpen}></AnimatedFilterIcon></button
>
</div>
{/if} {/if}
<section class="section"> <section class="section">
@ -152,90 +180,115 @@
</div> </div>
</section> </section>
<section class={isMobile && sidebarOpen <section
? "section sidebar open controls" class={isMobile && sidebarOpen
: isMobile && !sidebarOpen ? 'section sidebar open controls'
? "section sidebar closed controls" : isMobile && !sidebarOpen
: "section sticky controls"}> ? 'section sidebar closed controls'
<Controls {cases} ></Controls> : 'section sticky controls'}
>
<Controls {cases}></Controls>
</section> </section>
<section class="section"> <section class="section">
<div> <div>
{#if isMobile} {#if isMobile}
<TimelineMobile {cases}></TimelineMobile> <TimelineMobile {cases}></TimelineMobile>
{:else} {:else}
<Timeline {cases} {events} {metrics}></Timeline> <Timeline {cases} {events} {metrics}></Timeline>
{/if} {/if}
</div> </div>
</section> </section>
<section class="section"> <section class="section">
<div class="container"> <div class="container grid is-col-min-12">
<div class="field has-addons"> <div class="field has-addons">
<div class="buttons has-addons"> <div class="buttons has-addons">
<button class={displayDataAs == "Table" ? "button is-dark is-selected is-small" : "button is-small"} on:click={() => {displayDataAs = "Table"}}>Table</button> <button
<button class={displayDataAs == "Cards" ? "button is-dark is-selected is-small" : "button is-small"} on:click={() => {displayDataAs = "Cards"}}>Cards</button> class={displayDataAs == 'Table'
</div> ? 'button is-dark is-selected is-small'
</div> : 'button is-small'}
</div> on:click={() => {
</section> displayDataAs = 'Table';
}}>Table</button
{#if displayDataAs == "Cards"} >
<section class="section"> <button
<div class="container"> class={displayDataAs == 'Cards'
<div class="grid is-col-min-12"> ? 'button is-dark is-selected is-small'
{#each cases as attrCase} : 'button is-small'}
{#if attrCase.show} on:click={() => {
<div class="cell"> displayDataAs = 'Cards';
<CaseCard cardData={attrCase}></CaseCard> }}>Cards</button
</div> >
{/if} </div>
{/each}
</div> </div>
<div class="select is-small">
<select bind:value={selectedSorting}>
{#each sortOptions as sortOpt}
<option value={sortOpt}>
{sortOpt.label}
</option>
{/each}
</select>
</div>
</div> </div>
</section> </section>
{#if displayDataAs == 'Cards'}
<section class="section">
<div class="container">
<div class="grid is-col-min-12">
{#each cases as attrCase}
{#if attrCase.show}
<div class="cell">
<CaseCard cardData={attrCase}></CaseCard>
</div>
{/if}
{/each}
</div>
</div>
</section>
{/if} {/if}
{#if displayDataAs == "Table"} {#if displayDataAs == 'Table' && sortedCases.length > 0}
<section class="section"> <section class="section">
<div class="container"> <div class="container">
<CaseTable {cases}></CaseTable> <CaseTable cases={sortedCases}></CaseTable>
</div> </div>
</section> </section>
{/if} {/if}
<style> <style>
.intro { .intro {
max-width: 800px; max-width: 800px;
margin: auto; margin: auto;
} }
.controls { .controls {
background-color: #ffffffdd; background-color: #ffffffdd;
width: 100%; width: 100%;
z-index: 500; z-index: 500;
} }
.sticky { .sticky {
position: sticky; position: sticky;
top: 0px; top: 0px;
} }
.sidebar { .sidebar {
position: fixed; position: fixed;
top: 0px; top: 0px;
transition: left 0.5s; transition: left 0.5s;
height: 100vh; height: 100vh;
} }
.closed { .closed {
left: -100%; left: -100%;
} }
.open { .open {
left: 0; left: 0;
} }
.filter-button { .filter-button {
position: fixed; position: fixed;
top: 0; top: 0;
right: 0; right: 0;
padding: 1rem; padding: 1rem;
z-index: 750; z-index: 750;
} }
</style> </style>