Merge pull request #60 from reuters-graphics/table

Add Table component
This commit is contained in:
Jon McClure 2023-04-14 13:30:13 +01:00 committed by GitHub
commit 74447afa1d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 46530 additions and 3 deletions

44296
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,13 @@
import type { ComponentType } from 'svelte';
/**
* Used for the list of <option> tags nested in a <select> input.
*/
export type Option = {
value: string;
text: string;
};
/**
* Used for any props that restrict width of a container to one of pre-fab widths.
*/

View file

@ -60,7 +60,6 @@
display: block;
margin: 0;
padding: 0 15px;
overflow-x: hidden;
background-color: var(--theme-colour-background, transparent);
&.embedded {
overflow: auto;

View file

@ -0,0 +1,18 @@
<svg
class="icon"
aria-hidden="true"
focusable="false"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 6 11"
><path
d="m1.76 5.134 3.887-3.887a.71.71 0 0 0 0-1.027.709.709 0 0 0-1.027 0l-4.4 4.4a.71.71 0 0 0 0 1.027l4.4 4.4c.147.147.367.22.513.22a.79.79 0 0 0 .513-.22.71.71 0 0 0 0-1.027L1.76 5.133Z"
></path></svg
>
<style>
.icon {
height: 1rem;
width: 1rem;
fill: currentColor;
}
</style>

After

Width:  |  Height:  |  Size: 427 B

View file

@ -0,0 +1,17 @@
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.41069 12.0711C2.83695 11.5053 2.38137 10.8311 2.07042 10.0878C1.75947 9.3444 1.59934 8.54666 1.59934 7.74088C1.59934 6.9351 1.75947 6.13735 2.07042 5.39399C2.38137 4.65063 2.83695 3.97647 3.41069 3.41069C3.97647 2.83695 4.65063 2.38137 5.39399 2.07042C6.13735 1.75947 6.9351 1.59934 7.74088 1.59934C8.54666 1.59934 9.3444 1.75947 10.0878 2.07042C10.8311 2.38137 11.5053 2.83695 12.0711 3.41069C12.6448 3.97647 13.1004 4.65063 13.4113 5.39399C13.7223 6.13735 13.8824 6.9351 13.8824 7.74088C13.8824 8.54666 13.7223 9.3444 13.4113 10.0878C13.1004 10.8311 12.6448 11.5053 12.0711 12.0711C11.5053 12.6448 10.8311 13.1004 10.0878 13.4113C9.3444 13.7223 8.54666 13.8824 7.74088 13.8824C6.9351 13.8824 6.13735 13.7223 5.39399 13.4113C4.65063 13.1004 3.97647 12.6448 3.41069 12.0711ZM17.8254 16.6899L13.7454 12.6099C14.9941 11.0703 15.6041 9.11021 15.4497 7.13395C15.2953 5.1577 14.3882 3.3161 12.9156 1.98914C11.4429 0.662179 9.51715 -0.0489011 7.53554 0.00261584C5.55393 0.0541328 3.66769 0.864316 2.266 2.266C0.864316 3.66769 0.0541328 5.55393 0.00261584 7.53554C-0.0489011 9.51715 0.662179 11.4429 1.98914 12.9156C3.3161 14.3882 5.1577 15.2953 7.13395 15.4497C9.11021 15.6041 11.0703 14.9941 12.6099 13.7454L16.6899 17.8254C16.8453 17.9485 17.0405 18.0101 17.2384 17.9986C17.4363 17.9872 17.6231 17.9034 17.7633 17.7633C17.9034 17.6231 17.9872 17.4363 17.9986 17.2384C18.0101 17.0405 17.9485 16.8453 17.8254 16.6899Z"
></path>
</svg>
<style>
svg path {
fill: var(--theme-colour-text-secondary);
}
</style>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,130 @@
<script lang="ts">
import { intcomma } from 'journalize';
import LeftArrow from './LeftArrow.svelte';
import RightArrow from './RightArrow.svelte';
/**
* The current page number.
* @type {number}
*/
export let pageNumber: number = 1;
/**
* The default page size.
* @type {number}
*/
export let pageSize: number = 25;
/**
* The number of records in the current page.
* @type {number}
*/
export let pageLength: number = null;
/**
* The total number of records in the data set.
* @type {number}
*/
export let n: number = null;
$: minRow = pageNumber * pageSize - pageSize + 1;
$: maxRow = pageNumber * pageSize - pageSize + pageLength;
$: numPages = Math.ceil(n / pageSize);
function goToPreviousPage() {
if (pageNumber > 1) {
pageNumber -= 1;
}
}
function goToNextPage() {
if (pageNumber < numPages) {
pageNumber += 1;
}
}
</script>
<nav aria-label="pagination" class="pagination">
<button on:click="{goToPreviousPage}" disabled="{pageNumber === 1}"
><div class="icon-wrapper">
<LeftArrow />
<span class="visually-hidden">Previous page</span>
</div></button
>
<div class="label" aria-label="page {pageNumber}" aria-current="page">
<div class="records">{minRow}-{maxRow} of {intcomma(n)}</div>
</div>
<button
on:click="{goToNextPage}"
disabled="{pageNumber === Math.ceil(n / pageSize)}"
><div class="icon-wrapper">
<RightArrow />
<span class="visually-hidden">Next page</span>
</div></button
>
</nav>
<style lang="scss">
@import '../../scss/colours/thematic/tr';
@import '../../scss/fonts/variables';
nav.pagination {
display: flex;
justify-content: center;
align-items: center;
margin-top: 1rem;
button {
border: 1px solid var(--theme-colour-text-secondary, $tr-light-grey);
border-radius: 50%;
background: var(--theme-color-background);
color: var(--theme-colour-text-secondary, $tr-light-grey);
cursor: pointer;
width: 35px;
height: 35px;
&:disabled {
cursor: default;
color: var(--theme-colour-brand-rules);
border-color: var(--theme-colour-brand-rules);
.icon-wrapper:hover {
color: var(--theme-colour-brand-rules);
border-color: var(--theme-colour-brand-rules);
}
}
.icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
white-space: nowrap;
&:hover {
color: var(--theme-colour-text-primary, $tr-medium-grey);
border-color: var(--theme-colour-text-primary, $tr-medium-grey);
}
}
}
.label {
display: flex;
align-items: center;
flex-direction: column;
width: auto;
min-width: 110px;
margin: 0 0.5rem;
.records {
font-size: 0.8rem;
font-family: var(--theme-font-family-hed, $font-family-display);
font-weight: 300;
margin: 0 1rem;
color: var(--theme-colour-text-primary, $tr-medium-grey);
}
}
}
.visually-hidden {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
</style>

View file

@ -0,0 +1,18 @@
<svg
class="icon"
aria-hidden="true"
focusable="false"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 7 11"
><path
d="m6.013 4.987-4.4-4.4a.71.71 0 0 0-1.027 0 .709.709 0 0 0 0 1.027L4.473 5.5.586 9.387a.71.71 0 0 0 0 1.027c.147.147.293.22.513.22.22 0 .367-.073.514-.22l4.4-4.4a.71.71 0 0 0 0-1.027Z"
></path>
</svg>
<style>
.icon {
height: 1rem;
width: 1rem;
fill: currentColor;
}
</style>

After

Width:  |  Height:  |  Size: 427 B

View file

@ -0,0 +1,87 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import MagnifyingGlass from './MagnifyingGlass.svelte';
import X from './X.svelte';
/**
* The placeholder text that appears in the search box.
* @type {string}
*/
export let searchPlaceholder: string = 'Search in table';
let value = '';
$: active = value !== '';
const dispatch = createEventDispatcher();
function input(event) {
value = event.target.value;
dispatch('search', { value });
}
function clear() {
value = '';
dispatch('search', { value: '' });
}
</script>
<div class="search" class:active>
<div class="search--icon">
<MagnifyingGlass />
</div>
<input
id="search--input"
class="search--input"
type="text"
placeholder="{searchPlaceholder}"
on:input="{input}"
bind:value
/>
<div class="search--x" class:invisible="{!active}" on:click="{clear}">
<X />
</div>
</div>
<style lang="scss">
@import '../../scss/colours/thematic/tr';
@import '../../scss/fonts/variables';
.search {
position: relative;
display: inline-flex;
align-items: center;
width: 256px;
padding: 0 0 0 0.1rem;
.search--icon {
position: absolute;
left: 0.5rem;
top: 0.15rem;
width: 1.5rem;
height: 1.5rem;
fill: var(--theme-colour-brand-rules, $tr-muted-grey);
}
.search--input {
font-family: var(--theme-font-family-hed, $font-family-display);
color: var(--theme-colour-text-primary, $tr-dark-grey);
padding: 0 0 0 2rem;
font-size: 0.8rem;
height: 33px;
border: 1px solid var(--theme-colour-brand-rules, $tr-muted-grey);
background: transparent;
border-radius: 6px;
width: 100%;
}
.search--x {
position: absolute;
right: 0;
top: 0.15rem;
width: 1.5rem;
height: 1.5rem;
fill: var(--theme-colour-text-primary, $tr-medium-grey);
cursor: pointer;
&.invisible {
display: none;
}
}
}
</style>

View file

@ -0,0 +1,80 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import type { Option } from '../@types/global';
/**
* The label that appears above the select input.
* @type {string}
*/
export let label: string = '';
/**
* The label that appears above the select input.
* @type {Array}
*/
export let options: Option[] = [];
const dispatch = createEventDispatcher();
function input(event) {
const value = event.target.value;
dispatch('select', { value });
}
</script>
<div class="select">
{#if label}
<label for="select--input">{label}</label>
{/if}
<select
class="select--input"
name="select--input"
id="select--input"
on:input="{input}"
>
{#each options as obj}
<option value="{obj.value}">{obj.text}</option>
{/each}
</select>
</div>
<style lang="scss">
@import '../../scss/colours/thematic/tr';
@import '../../scss/fonts/variables';
.select {
width: 256px;
font-family: var(--theme-font-family-hed, $font-family-display);
label {
display: block;
font-size: 0.8rem;
font-weight: 300;
color: var(--theme-colour-text-primary, $tr-dark-grey);
padding: 0 0 0.125rem 0;
}
.select--input {
position: relative;
font-size: 0.8rem;
font-weight: 400;
line-height: 1;
height: 33px;
border: 1px solid var(--theme-colour-brand-rules, $tr-muted-grey);
color: var(--theme-colour-text-primary, $tr-dark-grey);
border-radius: 6px;
width: 100%;
padding: 0.5rem;
-moz-appearance: none; /* Firefox */
-webkit-appearance: none; /* Safari and Chrome */
appearance: none; /* Remove the default arrow */
padding-right: 20px; /* Add some padding to make room for a custom arrow */
background: transparent;
background-image: url('data:image/svg+xml;utf8,<svg width="15" height="9" viewBox="0 0 15 9" xmlns="http://www.w3.org/2000/svg"><path d="M6.76474 8.30466L0.236082 1.54523C-0.0786943 1.21934 -0.0786943 0.69069 0.236082 0.364804C0.550521 0.0392666 1.19794 0.0403099 1.51305 0.364804L7.33483 6.49522L12.9249 0.475171C13.3549 0.0451683 14.1195 0.0396141 14.4339 0.365152C14.7487 0.691037 14.7487 1.21969 14.4339 1.54557L7.90492 8.30466C7.59015 8.63054 7.07952 8.63054 6.76474 8.30466Z" fill="gray"/></svg>');
background-repeat: no-repeat;
background-position-x: 235px;
background-position-y: 55%;
}
.select--input::-ms-expand {
display: none; /* Remove the default arrow in Internet Explorer 11 */
}
}
</style>

View file

@ -0,0 +1,45 @@
<script lang="ts">
/**
* The direction of the sort. By default it's ascending.
* @type {SortDirection}
*/
type SortDirection = 'ascending' | 'descending';
export let sortDirection: SortDirection = 'ascending';
/**
* Whether or not this arrow is currently sorting. It is false by default.
* @type {boolean}
*/
export let active: boolean = false;
</script>
<svg
width="15"
height="21"
viewBox="0 0 15 21"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="avoid-clicks"
>
<path
class:active="{sortDirection === 'descending' && active}"
d="M6.76474 20.2244L0.236082 13.4649C-0.0786943 13.139 -0.0786943 12.6104 0.236082 12.2845C0.550521 11.959 1.19794 11.96 1.51305 12.2845L7.33483 12.2845L13 12.2845C13.43 11.8545 14.1195 11.9593 14.4339 12.2849C14.7487 12.6107 14.7487 13.1394 14.4339 13.4653L7.90492 20.2244C7.59015 20.5503 7.07952 20.5503 6.76474 20.2244Z"
></path>
<path
class:active="{sortDirection === 'ascending' && active}"
d="M7.90518 0.244414L14.4338 7.00385C14.7486 7.32973 14.7486 7.85838 14.4338 8.18427C14.1194 8.50981 13.472 8.50876 13.1569 8.18427L7.33509 8.18427L1.66992 8.18427C1.23992 8.61427 0.550443 8.50946 0.236003 8.18392C-0.0787725 7.85803 -0.0787725 7.32938 0.236003 7.0035L6.765 0.244414C7.07978 -0.0814713 7.5904 -0.0814713 7.90518 0.244414Z"
></path>
</svg>
<style lang="scss">
@import '../../scss/colours/thematic/tr';
.avoid-clicks {
pointer-events: none;
}
path {
fill: var(--theme-colour-brand-rules, $tr-muted-grey);
&.active {
fill: var(--theme-colour-text-primary, $tr-dark-grey);
}
}
</style>

View file

@ -0,0 +1,174 @@
<script>
import { Meta, Template, Story } from '@storybook/addon-svelte-csf';
// Don't lose the "?raw" in markdown imports!
// @ts-ignore
import componentDocs from './stories/docs/component.md?raw';
// @ts-ignore
import metadataDocs from './stories/docs/metadata.md?raw';
// @ts-ignore
import truncateDocs from './stories/docs/truncate.md?raw';
// @ts-ignore
import paginateDocs from './stories/docs/paginate.md?raw';
// @ts-ignore
import searchDocs from './stories/docs/search.md?raw';
// @ts-ignore
import filterDocs from './stories/docs/filter.md?raw';
// @ts-ignore
import bothDocs from './stories/docs/both.md?raw';
// @ts-ignore
import sortDocs from './stories/docs/sort.md?raw';
// @ts-ignore
import formatDocs from './stories/docs/format.md?raw';
// @ts-ignore
import styleDocs from './stories/docs/style.md?raw';
import Table from './Table.svelte';
import { withComponentDocs, withStoryDocs } from '$docs/utils/withParams.js';
// You can import JSON you need in stories directly in code!
// @ts-ignore
import pressFreedom from './stories/pressFreedom.json';
import homeRuns from './stories/homeRuns.json';
import richestWomen from './stories/richestWomen.json';
const meta = {
title: 'Components/Table',
component: Table,
...withComponentDocs(componentDocs),
// https://storybook.js.org/docs/svelte/essentials/controls
argTypes: {
width: {
control: 'select',
options: ['normal', 'wide', 'wider', 'widest', 'fluid'],
},
},
};
</script>
<Meta {...meta} />
<Template let:args>
<Table {...args} />
</Template>
<Story
name="Default"
args="{{
width: 'normal',
data: homeRuns,
}}"
/>
<Story
name="Metadata"
{...withStoryDocs(metadataDocs)}
args="{{
width: 'normal',
data: homeRuns,
title: 'Career home run leaders',
dek: 'In baseball, a home run (also known as a "dinger" or "tater") occurs when a batter hits the ball over the outfield fence. When a home run is hit, the batter and any runners on base are able to score.',
notes: 'Note: As of Opening Day 2023',
source: 'Source: Baseball Reference',
}}"
/>
<Story
name="Truncate"
{...withStoryDocs(truncateDocs)}
args="{{
data: homeRuns,
truncated: true,
source: 'Source: Baseball Reference',
}}"
/>
<Story
name="Paginate"
{...withStoryDocs(paginateDocs)}
args="{{
data: pressFreedom,
title: 'Press Freedom Index',
paginated: true,
source: 'Source: Reporters Without Borders',
}}"
/>
<Story
name="Search"
{...withStoryDocs(searchDocs)}
args="{{
data: pressFreedom,
searchable: true,
paginated: true,
title: 'Press Freedom Index',
source: 'Source: Reporters Without Borders',
}}"
/>
<Story
name="Filter"
{...withStoryDocs(filterDocs)}
args="{{
data: pressFreedom,
paginated: true,
filterField: 'Region',
title: 'Press Freedom Index',
notes: 'Source: Reporters Without Borders',
}}"
/>
<Story
name="Search and filter"
{...withStoryDocs(bothDocs)}
args="{{
data: pressFreedom,
searchable: true,
filterField: 'Region',
paginated: true,
title: 'Press Freedom Index',
dek: 'Reporters Without Borders ranks countries based on their level of press freedom using criteria such as the degree of media pluralism and violence against journalists.',
source: 'Source: Reporters Without Borders',
}}"
/>
<Story
name="Sort"
{...withStoryDocs(sortDocs)}
args="{{
data: pressFreedom,
sortable: true,
sortField: 'Score',
sortDirection: 'descending',
paginated: true,
title: 'Press Freedom Index',
source: 'Source: Reporters Without Borders',
}}"
/>
<Story
name="Format"
{...withStoryDocs(formatDocs)}
args="{{
data: richestWomen,
title: 'The Richest Women in the World',
source: 'Source: Forbes',
sortable: true,
sortField: 'Net worth (in billions)',
sortDirection: 'descending',
fieldFormatters: { 'Net worth (in billions)': (v) => '$' + v.toFixed(1) },
}}"
/>
<Story
name="Style"
{...withStoryDocs(styleDocs)}
args="{{
id: 'custom-table',
data: richestWomen,
title: 'The Richest Women in the World',
source: 'Source: Forbes',
}}"
,
/>

View file

@ -0,0 +1,477 @@
<!-- @component `Table` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-Table--default) -->
<script lang="ts">
import { onMount } from 'svelte';
/**
* A source for the data.
* @type []
* @required
*/
export let data: [];
/**
* A title that runs above the table.
* @type {string}
*/
export let title: string | null = null;
/**
* A block of text that runs above the table.
* @type {string}
*/
export let dek: string | null = null;
/**
* A footnote that runs below the table.
* @type {string}
*/
export let notes: string | null = null;
/**
* A source line that runs below the table.
* @type {string}
*/
export let source: string | null = null;
/**
* list of the fields to include in the table. By default everything goes.
* @type []
*/
export let includedFields: string[] = Object.keys(data[0]).filter(
(f) => f !== 'searchStr'
);
/**
* Whether or not the table is cutoff after a set number of rows.
* @type {boolean}
*/
export let truncated: boolean = false;
/**
* If the table is truncated, how many rows to allow before the cutoff.
* @type {number}
*/
export let truncateLength: number = 5;
/**
* Whether or not the table is paginated after a set number of rows.
* @type {boolean}
*/
export let paginated: boolean = false;
/**
* The default page size.
* @type {number}
*/
export let pageSize: number = 25;
/**
* Whether or not searches are allowed.
* @type {boolean}
*/
export let searchable: boolean = false;
/**
* The placeholder text that appears in the search box.
* @type {string}
*/
export let searchPlaceholder: string = 'Search in table';
/**
* A field to offer uses as an interactive filter.
* @type {string}
*/
export let filterField: string;
/**
* The label to place above the filter box
* @type {string}
*/
export let filterLabel: string;
/**
* Whether or not sorts are allowed.
* @type {boolean}
*/
export let sortable: boolean = false;
/**
* The column to sort by. By default it's the first header.
* @type {string}
*/
export let sortField: string = Object.keys(data[0])[0];
/**
* The columns that are allowed to sort. It's all of them by default.
* @type {string}
*/
export let sortableFields: string[] = Object.keys(data[0]).filter(
(f) => f !== 'searchStr'
);
/**
* The direction of the sort. By default it's ascending.
* @type {SortDirection}
*/
type SortDirection = 'ascending' | 'descending';
export let sortDirection: SortDirection = 'ascending';
/**
* Custom field formatting functions. Should be keyed to the name of the field.
* @type {object}
*/
export let fieldFormatters: object = {};
/** Width of the component within the text well. */
type ContainerWidth = 'normal' | 'wide' | 'wider' | 'widest' | 'fluid';
export let width: ContainerWidth = 'normal';
/** Add an ID to target with SCSS. */
export let id: string = '';
/** Add a class to target with SCSS. */
export let cls: string = '';
/** Import local helpers */
import Block from '../Block/Block.svelte';
import Pagination from './Pagination.svelte';
import Search from './Search.svelte';
import Select from './Select.svelte';
import SortArrow from './SortArrow.svelte';
import { filterArray, paginateArray, getOptions } from './utils.js';
/** Set truncate, filtering and pagination configuration */
let showAll = false;
let pageNumber = 1;
let searchText = '';
const filterList = filterField ? getOptions(data, filterField) : undefined;
let filterValue = '';
$: filteredData = filterArray(data, searchText, filterField, filterValue);
$: sortedData = sortArray(filteredData, sortField, sortDirection);
$: currentPageData = truncated
? showAll
? sortedData
: sortedData.slice(0, truncateLength + 1)
: paginated
? paginateArray(sortedData, pageSize, pageNumber)
: sortedData;
//* * Handle show all, search, filter, sort and pagination events */
function toggleTruncate(event) {
showAll = !showAll;
}
function handleSearchInput(event) {
searchText = event.detail.value;
pageNumber = 1;
}
function handleFilterInput(event) {
const value = event.detail.value;
filterValue = value === 'All' ? '' : value;
pageNumber = 1;
}
function handleSort(event) {
if (!sortable) return;
sortField = event.target.getAttribute('data-field');
sortDirection = sortDirection === 'ascending' ? 'descending' : 'ascending';
}
function sortArray(array, column, direction) {
if (!sortable) return array;
return array.sort((a, b) => {
if (a[column] < b[column]) {
return direction === 'ascending' ? -1 : 1;
} else if (a[column] > b[column]) {
return direction === 'ascending' ? 1 : -1;
} else {
return 0;
}
});
}
function formatValue(item, field) {
const value = item[field];
if (field in fieldFormatters) {
const func = fieldFormatters[field];
return func(value);
} else {
return value;
}
}
/** Boot it up. */
onMount(() => {
data.forEach((d) => {
// Compose the string we will allow users to search
d.searchStr = includedFields
.map((field) => d[field])
.join(' ')
.toLowerCase();
});
});
</script>
<Block width="{width}" id="{id}" cls="{cls}">
<article class="table-wrapper">
{#if title || dek || searchable || filterList}
<header class="table--header">
{#if title}
<h2 class="table--header--title">{@html title}</h2>
{/if}
{#if dek}
<p class="table--header--dek">{@html dek}</p>
{/if}
{#if searchable || filterList}
<nav class="input">
{#if filterList}
<div class="table--header--filter">
<Select
label="{filterLabel || filterField}"
options="{filterList}"
on:select="{handleFilterInput}"
/>
</div>
{/if}
{#if searchable}
<div class="table--header--search">
<Search
bind:searchPlaceholder
on:search="{handleSearchInput}"
/>
</div>
{/if}
</nav>
{/if}
</header>
{/if}
<section class="table">
<table
class:paginated
class:truncated="{truncated &&
!showAll &&
data.length > truncateLength}"
>
<thead class="table--thead">
<tr>
{#each includedFields as field}
<th
scope="col"
class="table--thead--th"
class:sortable="{sortable && sortableFields.includes(field)}"
class:sort-ascending="{sortable &&
sortField === field &&
sortDirection === 'ascending'}"
class:sort-descending="{sortable &&
sortField === field &&
sortDirection === 'descending'}"
data-field="{field}"
on:click="{handleSort}"
>
{field}
{#if sortable && sortableFields.includes(field)}
<div class="table--thead--sortarrow avoid-clicks">
<SortArrow
bind:sortDirection
active="{sortField === field}"
/>
</div>
{/if}
</th>
{/each}
</tr>
</thead>
<tbody class="table--tbody">
{#each currentPageData as item, idx}
<tr data-row-index="{idx}">
{#each includedFields as field}
<td
data-row-index="{idx}"
data-field="{field}"
data-value="{item[field]}"
>
{@html formatValue(item, field)}
</td>
{/each}
</tr>
{/each}
{#if searchable && searchText && currentPageData.length === 0}
<tr>
<td class="no-results" colspan="{includedFields.length}">
No results found for "{searchText}"
</td>
</tr>
{/if}
</tbody>
{#if notes || source}
<tfoot class="table--tfoot">
{#if notes}
<tr>
<td colspan="{includedFields.length}">{@html notes}</td>
</tr>
{/if}
{#if source}
<tr>
<td colspan="{includedFields.length}">{@html source}</td>
</tr>
{/if}
</tfoot>
{/if}
</table>
</section>
{#if truncated && data.length > truncateLength}
<nav aria-label="Show all button" class="show-all">
<button on:click="{toggleTruncate}"
>{#if showAll}Show fewer rows{:else}Show {data.length -
truncateLength} more rows{/if}</button
>
</nav>
{/if}
{#if paginated}
<Pagination
bind:pageNumber
bind:pageSize
bind:pageLength="{currentPageData.length}"
bind:n="{sortedData.length}"
/>{/if}
</article>
</Block>
<style lang="scss">
@import '../../scss/colours/thematic/tr';
@import '../../scss/fonts/variables';
.table-wrapper {
font-size: 1rem;
font-family: var(--theme-font-family-hed, $font-family-display);
color: var(--theme-colour-text-primary, $tr-dark-grey);
}
.table--header {
width: 100%;
h2.table--header--title {
font-weight: 500;
color: var(--theme-colour-text-primary, $tr-dark-grey);
font-size: 1.33rem;
padding: 0;
margin: 0.5rem 0;
}
p.table--header--dek {
font-family: var(--theme-font-family-hed, $font-family-display);
color: var(--theme-colour-text-primary, $tr-dark-grey);
font-size: 1rem;
font-weight: 300;
line-height: 1.4;
padding: 0;
margin: 0.5rem 0;
}
}
section.table {
overflow-x: auto;
}
section.table table {
background-color: transparent;
border-collapse: separate;
border-spacing: 0;
width: 100%;
thead {
tr {
th {
border-bottom: 1px solid
var(--theme-colour-text-primary, $tr-medium-grey);
color: var(--theme-colour-text-primary, $tr-medium-grey);
background-color: var(--theme-colour-background, #fff);
font-size: 0.85rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.06rem;
line-height: 1.4;
padding: 0.5rem 0.25rem 0.5rem 0;
&.sortable {
cursor: pointer;
}
.table--thead--sortarrow {
display: inline-block;
margin: 0 0 0 0.125rem;
}
}
}
}
tbody {
td {
font-size: 1rem;
font-weight: 300;
padding: 0.5rem 0.25rem 0.5rem 0;
vertical-align: top;
border-bottom: 1px solid var(--theme-colour-brand-rules, $tr-muted-grey);
&.no-results {
color: var(--theme-colour-text-secondary, $tr-muted-grey);
}
}
}
tfoot.table--tfoot {
display: table-row;
tr {
border-bottom: 0;
}
td {
font-weight: 300;
color: var(--theme-colour-text-primary, $tr-dark-grey);
font-size: 0.8rem;
padding: 0.5rem 0 0 0;
}
}
&.truncated {
tbody tr:last-child:not(:first-child) {
border-bottom: none;
mask-image: linear-gradient(
to bottom,
var(--theme-colour-text-primary) 0%,
transparent 100%
);
-webkit-mask-image: linear-gradient(
to bottom,
var(--theme-colour-text-primary) 0%,
transparent 100%
);
}
}
}
.avoid-clicks {
pointer-events: none;
}
nav.input {
margin: 0.5rem 0;
padding: 0;
width: 100%;
display: flex;
justify-content: flex-start;
align-items: flex-end;
flex-direction: row;
flex-wrap: wrap;
gap: 1rem;
}
nav.show-all {
display: flex;
justify-content: center;
align-items: center;
margin-top: 1rem;
button {
font-size: 0.8rem;
font-family: var(--theme-font-family-hed, $font-family-display);
font-weight: 500;
min-width: 175px;
padding: 0.33rem 0.5rem;
border: 1px solid var(--theme-colour-brand-rules, $tr-muted-grey);
border-radius: 4px;
background: var(--theme-colour-background);
color: var(--theme-colour-text-primary, $tr-medium-grey);
cursor: pointer;
}
}
</style>

View file

@ -0,0 +1,21 @@
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_25_237)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0.300421 0.292131C0.392053 0.199685 0.50109 0.126304 0.621235 0.0762269C0.741381 0.0261493 0.870256 0.000366211 1.00042 0.000366211C1.13059 0.000366211 1.25946 0.0261493 1.37961 0.0762269C1.49975 0.126304 1.60879 0.199685 1.70042 0.292131L7.00082 5.45346L12.3012 0.292131C12.3929 0.199685 12.5019 0.126304 12.622 0.0762269C12.7422 0.0261493 12.8711 0.000366211 13.0012 0.000366211C13.1314 0.000366211 13.2603 0.0261493 13.3804 0.0762269C13.5006 0.126304 13.6096 0.199685 13.7012 0.292131C13.7954 0.378994 13.8706 0.48445 13.922 0.601837C13.9734 0.719225 13.9999 0.845993 13.9998 0.974136C13.9997 1.10228 13.973 1.22901 13.9215 1.34633C13.8699 1.46364 13.7946 1.569 13.7003 1.65573L8.21229 6.99906L13.7003 12.3443C14.0708 12.7064 14.0979 13.2365 13.7796 13.622L13.6994 13.7079C13.6077 13.8003 13.4987 13.8737 13.3785 13.9238C13.2584 13.9738 13.1295 13.9996 12.9994 13.9996C12.8692 13.9996 12.7403 13.9738 12.6202 13.9238C12.5 13.8737 12.391 13.8003 12.2994 13.7079L6.99895 8.54653L1.69855 13.7079C1.60692 13.8003 1.49789 13.8737 1.37774 13.9238C1.25759 13.9738 1.12872 13.9996 0.998554 13.9996C0.86839 13.9996 0.739515 13.9738 0.619369 13.9238C0.499223 13.8737 0.390186 13.8003 0.298554 13.7079C0.204346 13.621 0.129174 13.5155 0.0777863 13.3982C0.0263988 13.2808 -8.74916e-05 13.154 2.17122e-07 13.0259C8.79258e-05 12.8977 0.0267478 12.771 0.0782959 12.6537C0.129844 12.5364 0.20516 12.431 0.299488 12.3443L5.78655 7L0.300421 1.65573C-0.0710458 1.2936 -0.0981124 0.763464 0.220154 0.377998L0.300421 0.292131Z"
fill="var(--theme-colour-text-secondary)"></path>
</g>
<defs>
<clipPath id="clip0_25_237">
<rect width="14" height="14" fill="var(--theme-colour-background, white)"
></rect>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -0,0 +1,14 @@
Feel free to both search and filter.
```svelte
<Table
data="{yourData}"
,
searchable="{true}"
filterField="{'Region'}"
paginated="{true}"
title="{'Press Freedom Index'}"
dek="{'Reporters Without Borders ranks countries based on their level of press freedom using criteria such as the degree of media pluralism and violence against journalists.'}"
source="{'Source: Reporters Without Borders'}"
/>
```

View file

@ -0,0 +1,14 @@
Present structured data in a table. Consider making it interactive.
---
```svelte
<script>
import { Table } from '@reuters-graphics/graphics-components';
// Import your data as JSON, or otherwise structure it
import yourData from './stories/homeRuns.json';
</script>
<Table data="{yourData}" />
```

View file

@ -0,0 +1,11 @@
Allow users to filter the table by providing one of the attributes as the `filterField`. This works best with categorical columns.
```svelte
<Table
data="{yourData}"
filterField="{'Region'}"
paginated="{true}"
title="{'Press Freedom Index'}"
notes="{'Source: Reporters Without Borders'}"
/>
```

View file

@ -0,0 +1,21 @@
Format column values by supplying functions keyed to field names with the `fieldFormatters` option. Columns are still sorted using the raw, underlying values.
Among other things, this feature can be used to provide a unit of measurement with numeric fields.
```svelte
<script lang="ts">
const fieldFormatters = {
'Net worth (in billions)': (v) => '$' + v.toFixed(1),
};
</script>
<Table
data="{yourData}"
fieldFormatters="{fieldFormatters}"
sortable="{true}"
sortField="{'Score'}"
sortDirection="{'descending'}"
title="{'The Richest Women in the World'}"
source="{'Source: Forbes'}"
/>
```

View file

@ -0,0 +1,11 @@
Set the `title`, `dek`, `notes` and `source` options to add supporting metadata above and below the table.
```svelte
<Table
data="{yourData}"
title="{'Career home run leaders'}"
dek="{'In baseball, a home run (also known as a "dinger" or "tater") occurs when a batter hits the ball over the outfield fence. When a home run is hit, the batter and any runners on base are able to score.'}"
notes="{'Note: As of Opening Day 2023'}"
source="{'Source: Baseball Reference'}"
/>
```

View file

@ -0,0 +1,14 @@
When your table has lots of rows you should consider breaking it up into pages. This can be done by setting the `paginated` option.
When it is enabled, readers can leaf through the data using a set of buttons below the table. By default there are 25 records per page. You can change the number by adjusting the `pageSize` option.
This is a good option when publishing large tables for readers to explore. It works well with interactive features like searching and filters.
```svelte
<Table
data="{yourData}"
paginated="{true}"
title="{'Press Freedom Index'}"
source="{'Reporters Without Borders'}"
/>
```

View file

@ -0,0 +1,11 @@
Allow users to search the table by setting the optional `searchable` variable. Modify the default text that appears in the box by supplying the `searchPlaceholder` option.
```svelte
<Table
data="{yourData}"
searchable="{true}"
paginated="{true}"
title="{'Press Freedom Index'}"
notes="{'Source: Reporters Without Borders'}"
/>
```

View file

@ -0,0 +1,14 @@
Allow users to sort the table by setting the `sortable` input. Specify the starting order by setting `sortField` to a column name and `sortDirection` to `ascending` or `descending`.
By default, all fields are sortable. If you'd like to limit the columns where sorting is allowed, provide a list to the `sortableFields` option.
```svelte
<Table
data="{yourData}"
sortable="{true}"
sortField="{'Score'}"
sortDirection="{'descending'}"
title="{'Press Freedom Index'}"
source="{'Source: Reporters Without Borders'}"
/>
```

View file

@ -0,0 +1,23 @@
You can tailor the table's appearance by crafting CSS that targets specific elements.
Like other components, you can apply labels by providing the `id` of `cls` options, which allow you to make broad changes that remain limited to your element.
Each column has a `data-field` attribute that contains the field's name. Use it to apply different styles to different fields. One common use is setting different text alignments on different columns.
```svelte
<Table
id="{'custom-table'}"
data="{yourData}"
title="{'The Richest Women in the World'}"
source="{'Source: Forbes'}"
/>
<style lang="scss">
/* Here we right align the table's numeric column. */
#custom-table {
[data-field='Net worth (in billions)'] {
text-align: right;
}
}
</style>
```

View file

@ -0,0 +1,11 @@
When you table has 10 or more rows, consider clipping it by setting the `truncated` option. When it is enabled, the table is clipped and readers must request to see all rows by clicking a button below the table. By default this configuration will limit the table to five records. You can change the cutoff point by adjusting the `truncateLength` option.
This is a good option for simple tables with row counts between 10 and 30. It works best when the table doesn't require interactivity.
```svelte
<Table
data="{yourData}"
truncated="{true}"
source="{'Source: Baseball Reference'}"
/>
```

View file

@ -0,0 +1,42 @@
[
{
"Name": "Barry Bonds",
"Home runs": 762
},
{
"Name": "Hank Aaron",
"Home runs": 755
},
{
"Name": "Babe Ruth",
"Home runs": 714
},
{
"Name": "Albert Pujols",
"Home runs": 703
},
{
"Name": "Alex Rodriguez",
"Home runs": 696
},
{
"Name": "Willie Mays",
"Home runs": 660
},
{
"Name": "Ken Griffey Jr.",
"Home runs": 630
},
{
"Name": "Jim Thome",
"Home runs": 612
},
{
"Name": "Sammy Sosa",
"Home runs": 609
},
{
"Name": "Frank Robinson",
"Home runs": 586
}
]

View file

@ -0,0 +1,887 @@
[
{
"Country": "Afghanistan",
"Region": "Southern Asia",
"Score": 40.19
},
{
"Country": "Albania",
"Region": "Southern Europe",
"Score": 30.59
},
{
"Country": "Algeria",
"Region": "Northern Africa",
"Score": 47.26
},
{
"Country": "Andorra",
"Region": "Southern Europe",
"Score": 23.32
},
{
"Country": "Angola",
"Region": "Sub-Saharan Africa",
"Score": 34.06
},
{
"Country": "Argentina",
"Region": "Latin America and the Caribbean",
"Score": 28.99
},
{
"Country": "Armenia",
"Region": "Western Asia",
"Score": 28.83
},
{
"Country": "Australia",
"Region": "Australia and New Zealand",
"Score": 19.79
},
{
"Country": "Austria",
"Region": "Western Europe",
"Score": 16.34
},
{
"Country": "Azerbaijan",
"Region": "Western Asia",
"Score": 58.77
},
{
"Country": "Bahrain",
"Region": "Western Asia",
"Score": 61.1
},
{
"Country": "Bangladesh",
"Region": "Southern Asia",
"Score": 49.71
},
{
"Country": "Belarus",
"Region": "Eastern Europe",
"Score": 50.82
},
{
"Country": "Belgium",
"Region": "Western Europe",
"Score": 11.69
},
{
"Country": "Belize",
"Region": "Latin America and the Caribbean",
"Score": 27.61
},
{
"Country": "Benin",
"Region": "Sub-Saharan Africa",
"Score": 38.18
},
{
"Country": "Bhutan",
"Region": "Southern Asia",
"Score": 28.86
},
{
"Country": "Bolivia",
"Region": "Latin America and the Caribbean",
"Score": 35.47
},
{
"Country": "Bosnia and Herzegovina",
"Region": "Southern Europe",
"Score": 28.34
},
{
"Country": "Botswana",
"Region": "Sub-Saharan Africa",
"Score": 23.25
},
{
"Country": "Brazil",
"Region": "Latin America and the Caribbean",
"Score": 36.25
},
{
"Country": "Brunei",
"Region": "South-eastern Asia",
"Score": 49.91
},
{
"Country": "Bulgaria",
"Region": "Eastern Europe",
"Score": 37.29
},
{
"Country": "Burkina Faso",
"Region": "Sub-Saharan Africa",
"Score": 23.17
},
{
"Country": "Burundi",
"Region": "Sub-Saharan Africa",
"Score": 47.57
},
{
"Country": "Cambodia",
"Region": "South-eastern Asia",
"Score": 46.84
},
{
"Country": "Cameroon",
"Region": "Sub-Saharan Africa",
"Score": 43.78
},
{
"Country": "Canada",
"Region": "Northern America",
"Score": 15.25
},
{
"Country": "Cape Verde",
"Region": "Sub-Saharan Africa",
"Score": 20.09
},
{
"Country": "Central African Republic",
"Region": "Sub-Saharan Africa",
"Score": 41.92
},
{
"Country": "Chad",
"Region": "Sub-Saharan Africa",
"Score": 40.2
},
{
"Country": "Chile",
"Region": "Latin America and the Caribbean",
"Score": 27.89
},
{
"Country": "China",
"Region": "Eastern Asia",
"Score": 78.72
},
{
"Country": "Colombia",
"Region": "Latin America and the Caribbean",
"Score": 43.74
},
{
"Country": "Comoros",
"Region": "Sub-Saharan Africa",
"Score": 30.65
},
{
"Country": "Congo",
"Region": "Sub-Saharan Africa",
"Score": 38.83
},
{
"Country": "Costa Rica",
"Region": "Latin America and the Caribbean",
"Score": 8.76
},
{
"Country": "Cote d'Ivoire",
"Region": "Sub-Saharan Africa",
"Score": 28.87
},
{
"Country": "Croatia",
"Region": "Southern Europe",
"Score": 27.95
},
{
"Country": "Cuba",
"Region": "Latin America and the Caribbean",
"Score": 63.94
},
{
"Country": "Cyprus",
"Region": "Western Asia",
"Score": 19.85
},
{
"Country": "Czechia",
"Region": "Eastern Europe",
"Score": 23.38
},
{
"Country": "Democratic Republic of Congo",
"Region": "Sub-Saharan Africa",
"Score": 48.59
},
{
"Country": "Denmark",
"Region": "Northern Europe",
"Score": 8.57
},
{
"Country": "Djibouti",
"Region": "Sub-Saharan Africa",
"Score": 78.62
},
{
"Country": "Dominican Republic",
"Region": "Latin America and the Caribbean",
"Score": 25.6
},
{
"Country": "Ecuador",
"Region": "Latin America and the Caribbean",
"Score": 32.83
},
{
"Country": "Egypt",
"Region": "Northern Africa",
"Score": 56.17
},
{
"Country": "El Salvador",
"Region": "Latin America and the Caribbean",
"Score": 30.49
},
{
"Country": "Equatorial Guinea",
"Region": "Sub-Saharan Africa",
"Score": 55.67
},
{
"Country": "Eritrea",
"Region": "Sub-Saharan Africa",
"Score": 81.45
},
{
"Country": "Estonia",
"Region": "Northern Europe",
"Score": 15.25
},
{
"Country": "Eswatini",
"Region": "Sub-Saharan Africa",
"Score": 46.34
},
{
"Country": "Ethiopia",
"Region": "Sub-Saharan Africa",
"Score": 33.63
},
{
"Country": "Fiji",
"Region": "Melanesia",
"Score": 27.92
},
{
"Country": "Finland",
"Region": "Northern Europe",
"Score": 6.99
},
{
"Country": "France",
"Region": "Western Europe",
"Score": 22.6
},
{
"Country": "Gabon",
"Region": "Sub-Saharan Africa",
"Score": 38.6
},
{
"Country": "Gambia",
"Region": "Sub-Saharan Africa",
"Score": 30.76
},
{
"Country": "Georgia",
"Region": "Western Asia",
"Score": 28.64
},
{
"Country": "Germany",
"Region": "Western Europe",
"Score": 15.24
},
{
"Country": "Ghana",
"Region": "Sub-Saharan Africa",
"Score": 21.33
},
{
"Country": "Greece",
"Region": "Southern Europe",
"Score": 29.01
},
{
"Country": "Guatemala",
"Region": "Latin America and the Caribbean",
"Score": 38.45
},
{
"Country": "Guinea",
"Region": "Sub-Saharan Africa",
"Score": 35.42
},
{
"Country": "Guinea-Bissau",
"Region": "Sub-Saharan Africa",
"Score": 32.68
},
{
"Country": "Guyana",
"Region": "Latin America and the Caribbean",
"Score": 25.61
},
{
"Country": "Haiti",
"Region": "Latin America and the Caribbean",
"Score": 31.12
},
{
"Country": "Honduras",
"Region": "Latin America and the Caribbean",
"Score": 49.35
},
{
"Country": "Hong Kong",
"Region": "Eastern Asia",
"Score": 30.44
},
{
"Country": "Hungary",
"Region": "Eastern Europe",
"Score": 31.76
},
{
"Country": "Iceland",
"Region": "Northern Europe",
"Score": 15.37
},
{
"Country": "India",
"Region": "Southern Asia",
"Score": 46.56
},
{
"Country": "Indonesia",
"Region": "South-eastern Asia",
"Score": 37.4
},
{
"Country": "Iran",
"Region": "Southern Asia",
"Score": 72.7
},
{
"Country": "Iraq",
"Region": "Western Asia",
"Score": 55.57
},
{
"Country": "Ireland",
"Region": "Northern Europe",
"Score": 11.91
},
{
"Country": "Israel",
"Region": "Western Asia",
"Score": 30.9
},
{
"Country": "Italy",
"Region": "Southern Europe",
"Score": 23.39
},
{
"Country": "Jamaica",
"Region": "Latin America and the Caribbean",
"Score": 9.96
},
{
"Country": "Japan",
"Region": "Eastern Asia",
"Score": 28.88
},
{
"Country": "Jordan",
"Region": "Western Asia",
"Score": 42.89
},
{
"Country": "Kazakhstan",
"Region": "Central Asia",
"Score": 50.28
},
{
"Country": "Kenya",
"Region": "Sub-Saharan Africa",
"Score": 33.65
},
{
"Country": "Kuwait",
"Region": "Western Asia",
"Score": 34.36
},
{
"Country": "Kyrgyzstan",
"Region": "Central Asia",
"Score": 30.37
},
{
"Country": "Laos",
"Region": "South-eastern Asia",
"Score": 70.56
},
{
"Country": "Latvia",
"Region": "Northern Europe",
"Score": 19.26
},
{
"Country": "Lebanon",
"Region": "Western Asia",
"Score": 34.93
},
{
"Country": "Lesotho",
"Region": "Sub-Saharan Africa",
"Score": 31.61
},
{
"Country": "Liberia",
"Region": "Sub-Saharan Africa",
"Score": 33.36
},
{
"Country": "Libya",
"Region": "Northern Africa",
"Score": 55.73
},
{
"Country": "Liechtenstein",
"Region": "Western Europe",
"Score": 19.49
},
{
"Country": "Lithuania",
"Region": "Northern Europe",
"Score": 20.15
},
{
"Country": "Luxembourg",
"Region": "Western Europe",
"Score": 17.56
},
{
"Country": "Madagascar",
"Region": "Sub-Saharan Africa",
"Score": 28.24
},
{
"Country": "Malawi",
"Region": "Sub-Saharan Africa",
"Score": 28.8
},
{
"Country": "Malaysia",
"Region": "South-eastern Asia",
"Score": 39.47
},
{
"Country": "Maldives",
"Region": "Southern Asia",
"Score": 29.13
},
{
"Country": "Mali",
"Region": "Sub-Saharan Africa",
"Score": 33.5
},
{
"Country": "Malta",
"Region": "Southern Europe",
"Score": 30.46
},
{
"Country": "Mauritania",
"Region": "Sub-Saharan Africa",
"Score": 32.25
},
{
"Country": "Mauritius",
"Region": "Sub-Saharan Africa",
"Score": 28.74
},
{
"Country": "Mexico",
"Region": "Latin America and the Caribbean",
"Score": 46.71
},
{
"Country": "Moldova",
"Region": "Eastern Europe",
"Score": 31.61
},
{
"Country": "Mongolia",
"Region": "Eastern Asia",
"Score": 28.97
},
{
"Country": "Montenegro",
"Region": "Southern Europe",
"Score": 34.33
},
{
"Country": "Morocco",
"Region": "Northern Africa",
"Score": 43.94
},
{
"Country": "Mozambique",
"Region": "Sub-Saharan Africa",
"Score": 35.39
},
{
"Country": "Myanmar",
"Region": "South-eastern Asia",
"Score": 46.14
},
{
"Country": "Namibia",
"Region": "Sub-Saharan Africa",
"Score": 19.72
},
{
"Country": "Nepal",
"Region": "Southern Asia",
"Score": 34.62
},
{
"Country": "Netherlands",
"Region": "Western Europe",
"Score": 9.67
},
{
"Country": "New Zealand",
"Region": "Australia and New Zealand",
"Score": 10.04
},
{
"Country": "Nicaragua",
"Region": "Latin America and the Caribbean",
"Score": 39.98
},
{
"Country": "Niger",
"Region": "Sub-Saharan Africa",
"Score": 28.44
},
{
"Country": "Nigeria",
"Region": "Sub-Saharan Africa",
"Score": 39.69
},
{
"Country": "North Korea",
"Region": "Eastern Asia",
"Score": 81.28
},
{
"Country": "North Macedonia",
"Region": "Southern Europe",
"Score": 31.67
},
{
"Country": "Norway",
"Region": "Northern Europe",
"Score": 6.72
},
{
"Country": "Oman",
"Region": "Western Asia",
"Score": 43.37
},
{
"Country": "Pakistan",
"Region": "Southern Asia",
"Score": 46.86
},
{
"Country": "Palestine",
"Region": "Western Asia",
"Score": 43.18
},
{
"Country": "Panama",
"Region": "Latin America and the Caribbean",
"Score": 29.94
},
{
"Country": "Papua New Guinea",
"Region": "Melanesia",
"Score": 24.88
},
{
"Country": "Paraguay",
"Region": "Latin America and the Caribbean",
"Score": 33.52
},
{
"Country": "Peru",
"Region": "Latin America and the Caribbean",
"Score": 31.71
},
{
"Country": "Philippines",
"Region": "South-eastern Asia",
"Score": 45.64
},
{
"Country": "Poland",
"Region": "Eastern Europe",
"Score": 28.84
},
{
"Country": "Portugal",
"Region": "Southern Europe",
"Score": 10.11
},
{
"Country": "Qatar",
"Region": "Western Asia",
"Score": 42.6
},
{
"Country": "Romania",
"Region": "Eastern Europe",
"Score": 24.91
},
{
"Country": "Russia",
"Region": "Eastern Europe",
"Score": 48.71
},
{
"Country": "Rwanda",
"Region": "Sub-Saharan Africa",
"Score": 50.66
},
{
"Country": "Samoa",
"Region": "Polynesia",
"Score": 19.24
},
{
"Country": "Saudi Arabia",
"Region": "Western Asia",
"Score": 62.73
},
{
"Country": "Senegal",
"Region": "Sub-Saharan Africa",
"Score": 25.22
},
{
"Country": "Serbia",
"Region": "Southern Europe",
"Score": 32.03
},
{
"Country": "Seychelles",
"Region": "Sub-Saharan Africa",
"Score": 25.66
},
{
"Country": "Sierra Leone",
"Region": "Sub-Saharan Africa",
"Score": 29.61
},
{
"Country": "Singapore",
"Region": "South-eastern Asia",
"Score": 55.2
},
{
"Country": "Slovakia",
"Region": "Eastern Europe",
"Score": 23.02
},
{
"Country": "Slovenia",
"Region": "Southern Europe",
"Score": 23.1
},
{
"Country": "Somalia",
"Region": "Sub-Saharan Africa",
"Score": 55.47
},
{
"Country": "South Africa",
"Region": "Sub-Saharan Africa",
"Score": 21.59
},
{
"Country": "South Korea",
"Region": "Eastern Asia",
"Score": 23.43
},
{
"Country": "South Sudan",
"Region": "Sub-Saharan Africa",
"Score": 45.78
},
{
"Country": "Spain",
"Region": "Southern Europe",
"Score": 20.44
},
{
"Country": "Sri Lanka",
"Region": "Southern Asia",
"Score": 42.2
},
{
"Country": "Sudan",
"Region": "Northern Africa",
"Score": 52.93
},
{
"Country": "Suriname",
"Region": "Latin America and the Caribbean",
"Score": 16.95
},
{
"Country": "Sweden",
"Region": "Northern Europe",
"Score": 7.24
},
{
"Country": "Switzerland",
"Region": "Western Europe",
"Score": 10.55
},
{
"Country": "Syria",
"Region": "Western Asia",
"Score": 70.63
},
{
"Country": "Taiwan",
"Region": "Eastern Asia",
"Score": 23.86
},
{
"Country": "Tajikistan",
"Region": "Central Asia",
"Score": 55.52
},
{
"Country": "Tanzania",
"Region": "Sub-Saharan Africa",
"Score": 40.69
},
{
"Country": "Thailand",
"Region": "South-eastern Asia",
"Score": 45.22
},
{
"Country": "Timor",
"Region": "South-eastern Asia",
"Score": 29.11
},
{
"Country": "Togo",
"Region": "Sub-Saharan Africa",
"Score": 29.59
},
{
"Country": "Tonga",
"Region": "Polynesia",
"Score": 24.59
},
{
"Country": "Trinidad and Tobago",
"Region": "Latin America and the Caribbean",
"Score": 21.55
},
{
"Country": "Tunisia",
"Region": "Northern Africa",
"Score": 29.53
},
{
"Country": "Turkey",
"Region": "Western Asia",
"Score": 49.79
},
{
"Country": "Turkmenistan",
"Region": "Central Asia",
"Score": 80.03
},
{
"Country": "Uganda",
"Region": "Sub-Saharan Africa",
"Score": 41.19
},
{
"Country": "Ukraine",
"Region": "Eastern Europe",
"Score": 32.96
},
{
"Country": "United Arab Emirates",
"Region": "Western Asia",
"Score": 43.13
},
{
"Country": "United Kingdom",
"Region": "Northern Europe",
"Score": 21.59
},
{
"Country": "United States",
"Region": "Northern America",
"Score": 23.93
},
{
"Country": "Uruguay",
"Region": "Latin America and the Caribbean",
"Score": 16.38
},
{
"Country": "Uzbekistan",
"Region": "Central Asia",
"Score": 50.74
},
{
"Country": "Venezuela",
"Region": "Latin America and the Caribbean",
"Score": 47.6
},
{
"Country": "Vietnam",
"Region": "South-eastern Asia",
"Score": 78.46
},
{
"Country": "Yemen",
"Region": "Western Asia",
"Score": 62.35
},
{
"Country": "Zambia",
"Region": "Sub-Saharan Africa",
"Score": 38.21
},
{
"Country": "Zimbabwe",
"Region": "Sub-Saharan Africa",
"Score": 43.12
}
]

View file

@ -0,0 +1,42 @@
[
{
"Name": "Francoise Bettencourt Meyers",
"Net worth (in billions)": 74.8
},
{
"Name": "Alice Walton",
"Net worth (in billions)": 65.3
},
{
"Name": "Julia Koch",
"Net worth (in billions)": 60
},
{
"Name": "MacKenzie Scott",
"Net worth (in billions)": 43.6
},
{
"Name": "Jacqueline Mars",
"Net worth (in billions)": 31.7
},
{
"Name": "Gina Rinehart",
"Net worth (in billions)": 30.2
},
{
"Name": "Miriam Adelson",
"Net worth (in billions)": 27.5
},
{
"Name": "Susanne Klatten",
"Net worth (in billions)": 24.3
},
{
"Name": "Iris Fontbona",
"Net worth (in billions)": 22.8
},
{
"Name": "Abigail Johnson",
"Net worth (in billions)": 21.2
}
]

View file

@ -0,0 +1,36 @@
export function filterArray(data, searchText, filterField, filterValue) {
if (searchText) {
data = data.filter((item) => {
return item.searchStr.includes(searchText.toLowerCase());
});
}
if (filterValue && filterValue) {
data = data.filter((item) => {
return item[filterField] === filterValue;
});
}
return data;
}
export function paginateArray(array, pageSize, pageNumber) {
return array.slice((pageNumber - 1) * pageSize, pageNumber * pageSize);
}
export function uniqueAttr(array, attr) {
return array.map((e) => e[attr]).filter(unique);
}
export function unique(value, index, array) {
return array.indexOf(value) === index;
}
export function getOptions(data, attr) {
// Get all the unique values in the provided field. Sort it.
const attrList = uniqueAttr(data, attr).sort((a, b) => a.localeCompare(b));
// Tack 'All' as the front as the first option.
attrList.unshift('All');
// Convert the list into Option typed objects ready for our Select component
return attrList.map((a) => ({ text: a, value: a }));
}

View file

@ -1,6 +1,11 @@
img.feature {
max-width: 100%;
margin: 0 auto;
display: block;
}
}
#custom-table {
[data-field='Net worth (in billions)'] {
text-align: right;
}
}

View file

@ -28,6 +28,7 @@ export { default as SiteFooter } from './components/SiteFooter/SiteFooter.svelte
export { default as SiteHeader } from './components/SiteHeader/SiteHeader.svelte';
export { default as SiteHeadline } from './components/SiteHeadline/SiteHeadline.svelte';
export { default as Spinner } from './components/Spinner/Spinner.svelte';
export { default as Table } from './components/Table/Table.svelte';
export {
default as Theme,
// @ts-ignore