Skip to content

Commit

Permalink
add src/lib/ColorBar.svelte
Browse files Browse the repository at this point in the history
plus new demo page src/routes/(demos)/color-bar/+page.md
add support for objects as heatmap_values i.e. maps from element symbols to numbers, previous only number arrays were supported
  • Loading branch information
janosh committed Feb 17, 2023
1 parent 1921efe commit 1bff50c
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 56 deletions.
41 changes: 41 additions & 0 deletions src/lib/ColorBar.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script lang="ts">
import * as d3sc from 'd3-scale-chromatic'
export let text: string = ``
export let color_scale: ((x: number) => string) | null = null
export let text_side: 'left' | 'right' | 'top' | 'bottom' = `left`
export let style: string | null = null
export let text_style: string | null = null
export let wrapper_style: string | null = null
$: if (color_scale === null) {
color_scale = d3sc[`interpolate${text}`]
if (color_scale === undefined) console.error(`Color scale not found: ${text}`)
}
$: ramped = [...Array(10).keys()].map((idx) => color_scale?.(idx / 10))
$: flex_dir = {
left: `row`,
right: `row-reverse`,
top: `column`,
bottom: `column-reverse`,
}[text_side]
</script>

<div style:flex-direction={flex_dir} style={wrapper_style}>
{#if text}<span style={text_style}>{text}</span>{/if}
<div style:background="linear-gradient(to right, {ramped})" {style} />
</div>

<style>
div {
display: flex;
gap: 5pt;
width: max-content;
}
div > div {
border-radius: 2pt;
height: 1em;
width: 10em;
}
</style>
31 changes: 6 additions & 25 deletions src/lib/ColorScaleSelect.svelte
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
<script lang="ts">
import * as d3sc from 'd3-scale-chromatic'
import Select from 'svelte-multiselect'
import { ColorBar } from '.'
export let value: string | null = null
export let selected: string[] = [`Viridis`]
export let minSelect: number = 0
export let placeholder = `Select a color scale`
const options = Object.keys(d3sc)
.filter((key) => key.startsWith(`interpolate`))
.map((key) => key.replace(`interpolate`, ``))
const ramp_color_scale = (name: string) =>
[...Array(10).keys()].map((idx) => d3sc[`interpolate${name}`](idx / 10))
</script>

<Select
Expand All @@ -20,27 +19,9 @@
{minSelect}
bind:value
bind:selected
placeholder="Select a color scale"
{placeholder}
{...$$props}
>
<div slot="option" let:option>
{option}
<span style:background="linear-gradient(to right, {ramp_color_scale(option)})" />
</div>
<div slot="selected" let:option>
{option}
<span style:background="linear-gradient(to right, {ramp_color_scale(option)})" />
</div>
<ColorBar slot="option" let:option text={option} />
<ColorBar slot="selected" let:option text={option} />
</Select>

<style>
div {
display: flex;
justify-content: space-between;
gap: 5pt;
}
span {
border-radius: 2pt;
min-height: 1em;
min-width: 8em;
}
</style>
31 changes: 22 additions & 9 deletions src/lib/PeriodicTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { goto } from '$app/navigation'
import * as d3sc from 'd3-scale-chromatic'
import type { Category, ChemicalElement, PeriodicTableEvents } from '.'
import { ElementPhoto, ElementTile } from '.'
import { ElementPhoto, ElementTile, elem_symbols, type ElementSymbol } from '.'
import element_data from './element-data'
export let tile_props: {
Expand All @@ -16,7 +16,7 @@
export let disabled = false // disable hover and click events from updating active_element
// either array of length 118 (one heat value for each element) or object with
// element symbol as key and heat value as value
export let heatmap_values: number[] = []
export let heatmap_values: Record<ElementSymbol, number> | number[] = []
// links is either string with element property (name, symbol, number, ...) to use as link,
// or object with mapping element symbols to link
export let links: keyof ChemicalElement | Record<string, string> | null = null
Expand All @@ -29,14 +29,27 @@
type $$Events = PeriodicTableEvents // for type-safe event listening on this component
$: if (heatmap_values.length > 118) {
console.error(
`heatmap_values should be an array of length 118 or less, one for each` +
`element possibly omitting elements at the end, got ${heatmap_values.length}`
let heat_values: number[] = []
$: if (Array.isArray(heatmap_values)) {
if (heatmap_values.length > 118) {
console.error(
`heatmap_values is an array of numbers, length should be 118 or less, one for ` +
`each element possibly omitting elements at the end, got ${heatmap_values.length}`
)
} else heat_values = heatmap_values
} else if (typeof heatmap_values == `object`) {
const bad_keys = Object.keys(heatmap_values).filter(
(key) => !elem_symbols.includes(key)
)
if (bad_keys.length > 0) {
console.error(
`heatmap_values is an object, keys should be element symbols, got ${bad_keys}`
)
} else heat_values = elem_symbols.map((symbol) => heatmap_values[symbol])
}
$: cmap_max = Math.max(...heatmap_values)
$: cmap_max = Math.max(...heat_values)
$: set_active_element = (element: ChemicalElement | null) => () => {
if (disabled) return
Expand Down Expand Up @@ -70,7 +83,7 @@
typeof color_scale == `string` ? d3sc[`interpolate${color_scale}`] : color_scale
$: bg_color = (value: number | false): string | null => {
if (!heatmap_values?.length || !c_scale) return null
if (!heat_values?.length || !c_scale) return null
if (!value) return `transparent`
return c_scale(log ? Math.log(value) / Math.log(cmap_max) : value / cmap_max)
}
Expand All @@ -84,7 +97,7 @@

{#each element_data as element, idx}
{@const { column, row, category, name, symbol } = element}
{@const value = heatmap_values?.length > 0 && heatmap_values[idx]}
{@const value = heat_values[idx]}
{@const active =
active_category === category.replaceAll(` `, `-`) ||
active_element?.name === name}
Expand Down
29 changes: 17 additions & 12 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,25 @@ export { default as ScatterPlot } from './ScatterPlot.svelte'
export { default as ScatterPoint } from './ScatterPoint.svelte'
export { default as TableInset } from './TableInset.svelte'

export type Category =
| `actinide`
| `alkali metal`
| `alkaline earth metal`
| `diatomic nonmetal`
| `lanthanide`
| `metalloid`
| `noble gas`
| `polyatomic nonmetal`
| `post-transition metal`
| `transition metal`
export const categories = [
`actinide`,
`alkali metal`,
`alkaline earth metal`,
`diatomic nonmetal`,
`lanthanide`,
`metalloid`,
`noble gas`,
`polyatomic nonmetal`,
`post-transition metal`,
`transition metal`,
] as const

export type Category = typeof categories[number]

// prettier-ignore
export type ElementSymbol = 'H' | 'He' | 'Li' | 'Be' | 'B' | 'C' | 'N' | 'O' | 'F' | 'Ne' | 'Na' | 'Mg' | 'Al' | 'Si' | 'P' | 'S' | 'Cl' | 'Ar' | 'K' | 'Ca' | 'Sc' | 'Ti' | 'V' | 'Cr' | 'Mn' | 'Fe' | 'Co' | 'Ni' | 'Cu' | 'Zn' | 'Ga' | 'Ge' | 'As' | 'Se' | 'Br' | 'Kr' | 'Rb' | 'Sr' | 'Y' | 'Zr' | 'Nb' | 'Mo' | 'Tc' | 'Ru' | 'Rh' | 'Pd' | 'Ag' | 'Cd' | 'In' | 'Sn' | 'Sb' | 'Te' | 'I' | 'Xe' | 'Cs' | 'Ba' | 'La' | 'Ce' | 'Pr' | 'Nd' | 'Pm' | 'Sm' | 'Eu' | 'Gd' | 'Tb' | 'Dy' | 'Ho' | 'Er' | 'Tm' | 'Yb' | 'Lu' | 'Hf' | 'Ta' | 'W' | 'Re' | 'Os' | 'Ir' | 'Pt' | 'Au' | 'Hg' | 'Tl' | 'Pb' | 'Bi' | 'Po' | 'At' | 'Rn' | 'Fr' | 'Ra' | 'Ac' | 'Th' | 'Pa' | 'U' | 'Np' | 'Pu' | 'Am' | 'Cm' | 'Bk' | 'Cf' | 'Es' | 'Fm' | 'Md' | 'No' | 'Lr' | 'Rf' | 'Db' | 'Sg' | 'Bh' | 'Hs' | 'Mt' | 'Ds' | 'Rg' | 'Cn' | 'Nh' | 'Fl' | 'Mc' | 'Lv' | 'Ts' | 'Og'
export const elem_symbols = [`H`,`He`,`Li`,`Be`,`B`,`C`,`N`,`O`,`F`,`Ne`,`Na`,`Mg`,`Al`,`Si`,`P`,`S`,`Cl`,`Ar`,`K`,`Ca`,`Sc`,`Ti`,`V`,`Cr`,`Mn`,`Fe`,`Co`,`Ni`,`Cu`,`Zn`,`Ga`,`Ge`,`As`,`Se`,`Br`,`Kr`,`Rb`,`Sr`,`Y`,`Zr`,`Nb`,`Mo`,`Tc`,`Ru`,`Rh`,`Pd`,`Ag`,`Cd`,`In`,`Sn`,`Sb`,`Te`,`I`,`Xe`,`Cs`,`Ba`,`La`,`Ce`,`Pr`,`Nd`,`Pm`,`Sm`,`Eu`,`Gd`,`Tb`,`Dy`,`Ho`,`Er`,`Tm`,`Yb`,`Lu`,`Hf`,`Ta`,`W`,`Re`,`Os`,`Ir`,`Pt`,`Au`,`Hg`,`Tl`,`Pb`,`Bi`,`Po`,`At`,`Rn`,`Fr`,`Ra`,`Ac`,`Th`,`Pa`,`U`,`Np`,`Pu`,`Am`,`Cm`,`Bk`,`Cf`,`Es`,`Fm`,`Md`,`No`,`Lr`,`Rf`,`Db`,`Sg`,`Bh`,`Hs`,`Mt`,`Ds`,`Rg`,`Cn`,`Nh`,`Fl`,`Mc`,`Lv`,`Ts`,`Og`] as const

export type ElementSymbol = typeof elem_symbols[number]

export type ChemicalElement = {
'cpk-hex': string | null
Expand Down
61 changes: 61 additions & 0 deletions src/routes/(demos)/color-bar/+page.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
`ColorBar.svelte`

ColorBar supports <code>text_side = ['top', 'bottom', 'left', 'right']</code>

```svelte example stackblitz
<script>
import { ColorBar } from '$lib'
import * as d3sc from 'd3-scale-chromatic'
const names = Object.keys(d3sc).filter((key) => key.startsWith(`interpolate`))
</script>
<section>
{#each [`top`, `bottom`, `left`, `right`] as text_side, idx}
<ul>
<code>{text_side}</code>
{#each names.slice(idx * 5, 5 * idx + 5) as name}
<li>
<ColorBar
text={name.replace(`interpolate`, ``)}
{text_side}
text_style="min-width: 5em;"
/>
</li>
{/each}
</ul>
{/each}
</section>
<style>
section {
display: flex;
overflow: scroll;
gap: 2em;
}
section > ul {
list-style: none;
padding: 0;
}
section > ul > li {
padding: 1ex;
}
section > ul > code {
font-size: 16pt;
}
</style>
```

You can also fat and skinny bars:

```svelte example stackblitz
<script>
import { ColorBar } from '$lib'
const wrapper_style = 'place-items: center;'
</script>
<ColorBar text="Viridis" {wrapper_style} style="width: 10em; height: 1ex;" />
<br />
<ColorBar text="Viridis" {wrapper_style} style="width: 3em; height: 2em;" />
```
5 changes: 2 additions & 3 deletions src/routes/(demos)/color-scales/+page.svx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@
let log = true // log color scale
let data = `MP`
let color_scale
$: heatmap_values = Object.values(data == `WBM` ? wbm_elem_counts : mp_elem_counts)
$: total = heatmap_values.reduce((a, b) => a + b, 0)

$: heatmap_values = data == `WBM` ? wbm_elem_counts : mp_elem_counts
$: total = Object.values(heatmap_values).reduce((a, b) => a + b, 0)
</script>


Expand Down
5 changes: 5 additions & 0 deletions tests/unit/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ test(`src/lib/icons/index.ts re-exports all icons`, () => {
)
expect(Object.keys(lib)).toEqual(expect.arrayContaining(components))
})

test(`categories and element_symbols are exported`, () => {
expect(lib.categories).toHaveLength(8)
expect(lib.elem_symbols).toHaveLength(lib.element_data.length)
})
29 changes: 24 additions & 5 deletions tests/unit/periodic-table.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,15 +125,15 @@ describe(`PeriodicTable`, () => {
element_tile?.dispatchEvent(new MouseEvent(`click`))
})

test.each([[`0`], [`10px`], [`1cqw`]])(`gap prop`, async (gap) => {
test.each([[`0`], [`10px`], [`1cqw`]])(`gap prop`, (gap) => {
new PeriodicTable({ target: document.body, props: { gap } })
const ptable = doc_query(`.periodic-table`)
expect(getComputedStyle(ptable).gap).toBe(gap)
})

test.each(Object.entries(category_counts))(
`setting active_category=%s highlights corresponding element tiles`,
async (active_category, expected_active) => {
(active_category, expected_active) => {
new PeriodicTable({
target: document.body,
props: { active_category: active_category.replaceAll(` `, `-`) },
Expand All @@ -146,7 +146,7 @@ describe(`PeriodicTable`, () => {

test.each([[[...Array(1000).keys()]], [[...Array(119).keys()]]])(
`raises error when receiving more than 118 heatmap values`,
async (heatmap_values) => {
(heatmap_values) => {
console.error = vi.fn()

new PeriodicTable({
Expand All @@ -156,8 +156,27 @@ describe(`PeriodicTable`, () => {

expect(console.error).toHaveBeenCalledOnce()
expect(console.error).toBeCalledWith(
`heatmap_values should be an array of length 118 or less, one for each` +
`element possibly omitting elements at the end, got ${heatmap_values.length}`
`heatmap_values is an array of numbers, length should be 118 or less, one for ` +
`each element possibly omitting elements at the end, got ${heatmap_values.length}`
)
}
)

test.each([{ foo: 42 }], [{}])(
`raises error when heatmap_values is object with unknown element symbols`,
(heatmap_values) => {
console.error = vi.fn()

new PeriodicTable({
target: document.body,
props: { heatmap_values },
})

expect(console.error).toHaveBeenCalledOnce()
expect(console.error).toBeCalledWith(
`heatmap_values is an object, keys should be element symbols, got ${Object.keys(
heatmap_values
)}`
)
}
)
Expand Down
4 changes: 2 additions & 2 deletions vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { sveltekit } from '@sveltejs/kit/vite'
import examples from 'mdsvexamples/vite'
import mdsvexamples from 'mdsvexamples/vite'
import type { UserConfig } from 'vite'
import type { UserConfig as VitestConfig } from 'vitest'

const vite_config: UserConfig & { test: VitestConfig } = {
plugins: [sveltekit(), examples],
plugins: [sveltekit(), mdsvexamples],

test: {
environment: `jsdom`,
Expand Down

0 comments on commit 1bff50c

Please sign in to comment.