Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Website options editor #195

Merged
merged 6 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading