Skip to content

Commit

Permalink
Merge pull request #195 from hatemhosny/website-options-editor
Browse files Browse the repository at this point in the history
Website options editor
  • Loading branch information
hatemhosny authored Sep 5, 2024
2 parents 0dccf85 + 3d2fd00 commit 9d47d7a
Show file tree
Hide file tree
Showing 18 changed files with 407 additions and 57 deletions.
37 changes: 25 additions & 12 deletions src/lib/race.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import * as d3 from './d3';
import { getDateString, prepareData, computeNextDateSubscriber, safeName } from './utils';
import type { Data, WideData } from './data';
import { createRenderer, rendererSubscriber, type Renderer } from './renderer';
import {
createRenderer,
createResizeObserver,
rendererSubscriber,
type Renderer,
} from './renderer';
import { createTicker } from './ticker';
import { styleInject } from './styles';
import { actions, createStore, rootReducer, type Store } from './store';
Expand Down Expand Up @@ -60,16 +65,17 @@ export async function race(
}

const events = registerEvents(store, ticker);
window.addEventListener('resize', resize);
const resizeObserver = createResizeObserver(resize);
resizeObserver?.observe(root);

function subscribeToStore(store: Store, renderer: Renderer, data: Data[]) {
const subscriptions = [
rendererSubscriber(store, renderer),
computeNextDateSubscriber(data, store),
DOMEventSubscriber(store),
];
[...subscriptions, ...apiSubscriptions].forEach((subcsription) => {
store.subscribe(subcsription);
[...subscriptions, ...apiSubscriptions].forEach((subscription) => {
store.subscribe(subscription);
});
}

Expand All @@ -88,7 +94,6 @@ export async function race(
}

const API = {
// TODO: validate user input
play() {
if (!store.getState().ticker.isRunning) {
ticker.start();
Expand All @@ -107,10 +112,12 @@ export async function race(
ticker.skipForward();
},
inc(value = 1) {
store.dispatch(actions.ticker.inc(+value));
if (!isNaN(Number(value))) value = 1;
store.dispatch(actions.ticker.inc(Number(value)));
},
dec(value = 1) {
store.dispatch(actions.ticker.dec(+value));
if (!isNaN(Number(value))) value = 1;
store.dispatch(actions.ticker.dec(Number(value)));
},
setDate(inputDate: string | Date) {
store.dispatch(actions.ticker.updateDate(getDateString(inputDate)));
Expand All @@ -128,13 +135,13 @@ export async function race(
d3.select(root)
.select('rect.' + safeName(name))
.classed('selected', true);
store.dispatch(actions.data.addSelection(name));
store.dispatch(actions.data.addSelection(String(name)));
},
unselect(name: string) {
d3.select(root)
.select('rect.' + safeName(name))
.classed('selected', false);
store.dispatch(actions.data.removeSelection(name));
store.dispatch(actions.data.removeSelection(String(name)));
},
unselectAll() {
d3.select(root).selectAll('rect').classed('selected', false);
Expand Down Expand Up @@ -206,12 +213,15 @@ export async function race(
}
},
onDate(date: string | Date, fn: ApiCallback) {
if (typeof fn !== 'function') {
throw new Error('The second argument must be a function');
}
const dateString = getDateString(date);
let lastDate = '';
const watcher = addApiSubscription(() => {
if (store.getState().ticker.currentDate === dateString && dateString !== lastDate) {
lastDate = store.getState().ticker.currentDate; // avoid infinite loop if fn dispatches action
fn.call(API, getTickDetails(store));
fn(getTickDetails(store));
}
lastDate = store.getState().ticker.currentDate;
});
Expand All @@ -222,8 +232,11 @@ export async function race(
};
},
on(event: EventType, fn: ApiCallback) {
if (typeof fn !== 'function') {
throw new Error('The second argument must be a function');
}
const watcher = events.addApiEventHandler(event, () => {
fn.call(API, getTickDetails(store));
fn(getTickDetails(store));
});
return {
remove() {
Expand All @@ -235,7 +248,7 @@ export async function race(
ticker.stop();
store.unsubscribeAll();
events.unregister();
window.removeEventListener('resize', resize);
resizeObserver?.unobserve(root);
root.innerHTML = '';
document.getElementById(stylesId)?.remove();
for (const method of Object.keys(this)) {
Expand Down
1 change: 1 addition & 0 deletions src/lib/renderer/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { createRenderer } from './create-renderer';
export { createResizeObserver } from './resize-observer';
export { elements } from './elements';
export { rendererSubscriber } from './renderer-subscriber';
export type { Renderer } from './renderer.models';
18 changes: 18 additions & 0 deletions src/lib/renderer/resize-observer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export function createResizeObserver(resizeFn: () => void) {
if (window.ResizeObserver && window.requestAnimationFrame) {
return new ResizeObserver((entries) => {
window.requestAnimationFrame((): void | undefined => {
if (!entries?.length) return;
resizeFn();
});
});
}
return {
observe: () => {
window.addEventListener('resize', resizeFn);
},
unobserve: () => {
window.removeEventListener('resize', resizeFn);
},
};
}
2 changes: 1 addition & 1 deletion src/lib/utils/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export async function prepareData(
const messageId = generateId();
worker.postMessage({
type: 'prepare-data',
data,
data: await data,
options: removeFnOptions(store.getState().options),
baseUrl: location.href,
messageId,
Expand Down
2 changes: 1 addition & 1 deletion src/lib/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export function getText(

export function safeName(name: string) {
// replace non-alphanumeric with underscore
return name.replace(/[\W]+/g, '_');
return String(name).replace(/[\W]+/g, '_');
}

export function toggleClass(root: HTMLElement, selector: string, className: string) {
Expand Down
2 changes: 1 addition & 1 deletion website/docs/documentation/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ See the guide on [`chart controls`](../guides/chart-controls.md) for other alter

### dataShape

Instruction whether the data shape is <a href="https://en.wikipedia.org/wiki/Wide_and_narrow_data" target="_blank">"long" or "wide"</a>. By default, the library tries to detect the data shape automatically from its structure (after any [transformation](#dataTransform), by finding the columns `date`, `name` and `value`). If the data shape is not detected correctly, it can be manually specified.
Instruction whether the data shape is <a href="https://en.wikipedia.org/wiki/Wide_and_narrow_data" target="_blank">"long" or "wide"</a>. By default, the library tries to detect the data shape automatically from its structure (after any [transformation](#datatransform), by finding the columns `date`, `name` and `value`). If the data shape is not detected correctly, it can be manually specified.
See ["Data" section](./data.md) for more details and examples.

- Type: `"long" | "wide" | "auto"`
Expand Down
32 changes: 5 additions & 27 deletions website/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ _RacingBars_ is an [open-source](https://github.com/hatemhosny/racing-bars), lig
_RacingBars_ is available for JavaScript, TypeScript, React, Vue and Svelte.

import RacingBars from '../src/components/RacingBars';
import ChartOptions from '../src/components/ChartOptions';

**Examples:**

This is a basic chart with the default options

<div className="gallery">
<RacingBars
dataUrl="/data/brands.json"
Expand All @@ -27,33 +30,8 @@ import RacingBars from '../src/components/RacingBars';

<p style={{height: 30}}> </p>

export const transformFn = (data) => data.map((d) => ({
...d,
icon: `https://flagsapi.com/${d.code}/flat/64.png`,
}));
Try playing with some of the [options](./documentation/options.md). Or check the code [playground](./playground).

<div className="gallery">
<RacingBars
style={{width: 800, height: 450}}
dataUrl="/data/population.csv"
dataTransform={transformFn}
title="World Population in 60 Years"
subTitle="Country Population in millions"
caption="Source: World Bank"
dateCounter= "YYYY"
showGroups={true}
showIcons={true}
labelsPosition="outside"
labelsWidth={160}
autorun={false}
overlays="all"
controlButtons="all"
highlightBars={true}
selectBars={true}
theme="dark"
dynamicProps={{dataTransform: `(data) => data.map((d) => ({
...d,
icon: \`https://flagsapi.com/\${d.code}/flat/64.png\`,
}))`}}
/>
<ChartOptions />
</div>
11 changes: 8 additions & 3 deletions website/docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ const config: Config = {
},
],
},
docs: {
sidebar: {
hideable: true,
},
},
footer: {
style: 'dark',
links: [
Expand Down Expand Up @@ -151,15 +156,15 @@ const config: Config = {
} satisfies Preset.ThemeConfig,
scripts: [
{
src: 'https://unpkg.com/prettier@2.4.1/standalone.js',
src: '/js/prettier-2.4.1/standalone.js',
async: true,
},
{
src: 'https://unpkg.com/prettier@2.4.1/parser-babel.js',
src: '/js/prettier-2.4.1/parser-babel.js',
async: true,
},
{
src: 'https://unpkg.com/prettier@2.4.1/parser-html.js',
src: '/js/prettier-2.4.1/parser-html.js',
async: true,
},
],
Expand Down
86 changes: 86 additions & 0 deletions website/src/components/ChartOptions/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// eslint-disable-next-line import/no-unresolved
import { getBaseUrl } from '@site/src/helpers/base-url';
import RacingBars from '../RacingBars';
import type { Props } from '../../../../src';
import styles from './styles.module.css';

const PARAMS: Props = {
dataTransform: (data) =>
data.map((d) => ({
...d,
icon: `https://flagsapi.com/${d.code}/flat/64.png`,
})),
colorSeed: '',
showGroups: false,
topN: 10,
autorun: false,
loop: false,
title: 'World Population',
subTitle: 'in millions',
caption: 'Source: World Bank',
dateCounter: 'MM/YYYY',
labelsPosition: 'inside',
labelsWidth: 150,
showIcons: true,
controlButtons: 'all',
overlays: 'all',
theme: 'dark',
fixedScale: false,
highlightBars: true,
selectBars: true,
};

async function initPane(racer) {
const baseUrl = getBaseUrl();
const mod = await import(/* webpackIgnore: true */ `${baseUrl}/js/tweakpane.min.js`);
const { Pane } = mod;
const pane = new Pane({
container: document.querySelector<HTMLElement>('#tweakpane')!,
expanded: true,
title: 'Change Options:',
});

pane.addBinding(PARAMS, 'theme', { options: { dark: 'dark', light: 'light' } });
pane.addBinding(PARAMS, 'title');
pane.addBinding(PARAMS, 'subTitle');
pane.addBinding(PARAMS, 'caption');
pane.addBinding(PARAMS, 'dateCounter');
pane.addBinding(PARAMS, 'showIcons');
pane.addBinding(PARAMS, 'showGroups');
pane.addBinding(PARAMS, 'topN', { min: 2, max: 15, step: 1 });
pane.addBinding(PARAMS, 'labelsPosition', {
options: { inside: 'inside', outside: 'outside' },
});
pane.addBinding(PARAMS, 'labelsWidth', { min: 0, max: 300, step: 1 });
pane.addBinding(PARAMS, 'loop');
pane.addBinding(PARAMS, 'controlButtons', {
options: { all: 'all', play: 'play', none: 'none' },
});
pane.addBinding(PARAMS, 'overlays', {
options: { all: 'all', play: 'play', repeat: 'repeat', none: 'none' },
});
pane.addBinding(PARAMS, 'fixedScale');
pane.addBinding(PARAMS, 'colorSeed');
pane.addBinding(PARAMS, 'highlightBars');
pane.addBinding(PARAMS, 'selectBars');

pane.on('change', (ev: any) => {
const key = ev.target.label;
racer.changeOptions({ [key]: ev.value });
});
}

export default function ChartOptions() {
return (
<div className={styles.container}>
<RacingBars
dataUrl="/data/population.csv"
{...PARAMS}
callback={(racer) => initPane(racer)}
showCode={false}
className={styles.chart}
/>
<div id="tweakpane" className={styles.tweakpane}></div>
</div>
);
}
17 changes: 17 additions & 0 deletions website/src/components/ChartOptions/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.container {
display: flex;
justify-content: center;
min-width: 100%;
gap: 1em;
}

.tweakpane {
width: 300px;
}

@media screen and (max-width: 600px) {
.container {
align-items: center;
flex-direction: column;
}
}
8 changes: 2 additions & 6 deletions website/src/components/OpenInPlayground/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
/* eslint-disable import/no-unresolved */
import React from 'react';
import { getPlaygroundUrl, type Config, type Language } from 'livecodes';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import { getBaseUrl } from '@site/src/helpers/base-url';
import styles from './styles.module.css';

export default function OpenInPlayground(props: { language: Language; code: string }) {
const baseUrl = ExecutionEnvironment.canUseDOM
? location.origin
: useDocusaurusContext().siteConfig.url;

const baseUrl = getBaseUrl();
const config: Partial<Config> = {
title: 'RacingBars',
activeEditor: 'script',
Expand Down
3 changes: 2 additions & 1 deletion website/src/components/RacingBars/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default function RacingBars(
label?: string;
},
): JSX.Element {
const { label, className, style, showCode, dynamicProps, ...options } = props;
const { label, className, style, showCode, dynamicProps, callback, ...options } = props;
const { jsCode, tsCode, reactCode, vueCode, svelteCode } = getFrameworkCode(
options,
dynamicProps,
Expand Down Expand Up @@ -55,6 +55,7 @@ export default function RacingBars(
}}
{...{
theme: colorMode,
callback,
...options,
}}
>
Expand Down
6 changes: 6 additions & 0 deletions website/src/helpers/base-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/* eslint-disable import/no-unresolved */
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';

export const getBaseUrl = () =>
ExecutionEnvironment.canUseDOM ? location.origin : useDocusaurusContext().siteConfig.url;
Loading

0 comments on commit 9d47d7a

Please sign in to comment.