Skip to content

Commit

Permalink
Implement selectors
Browse files Browse the repository at this point in the history
  • Loading branch information
AhmedElbohoty committed Aug 28, 2024
1 parent ca0db9e commit 125a1d2
Show file tree
Hide file tree
Showing 15 changed files with 228 additions and 95 deletions.
32 changes: 23 additions & 9 deletions src/lib/events/events.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { elements } from '../renderer';
import type { Store } from '../store';
import {
selectContElement,
selectOptKeyboardControls,
selectOptMouseControls,
selectTickCurrentDate,
selectTickIsFirstDate,
selectTickIsLastDate,
selectTickIsRunning,
type Store,
} from '../store';
import type { Ticker } from '../ticker';
import { hideElement, getElement, getClicks } from '../utils';
import type { DOMCustomEvent, EventType, Event, TickDetails } from './models';

export function registerEvents(store: Store, ticker: Ticker) {
const root = store.getState().container.element;
const root = selectContElement(store.getState());
const events: Event[] = [];
register();

Expand Down Expand Up @@ -43,7 +52,8 @@ export function registerEvents(store: Store, ticker: Ticker) {
}

function registerClickEvents() {
if (store.getState().options.mouseControls) {
const mouseControls = selectOptMouseControls(store.getState());
if (mouseControls) {
const svg = root.querySelector('svg') as SVGSVGElement;
svg.addEventListener('click', (clickEvent) => {
// ignore clicks to group legends
Expand All @@ -65,7 +75,7 @@ export function registerEvents(store: Store, ticker: Ticker) {
}

function registerKeyboardEvents() {
if (store.getState().options.keyboardControls) {
if (selectOptKeyboardControls(store.getState())) {
addEventHandler(document, '', 'keyup', handleKeyboardEvents);
}
}
Expand Down Expand Up @@ -154,7 +164,7 @@ export function getTickDetails(store: Store): TickDetails {
}

function dispatchDOMEvent(store: Store, eventType: EventType) {
const element = store.getState().container.element;
const element = selectContElement(store.getState());
if (!element) return;
element.dispatchEvent(
new CustomEvent(eventType, {
Expand All @@ -168,14 +178,18 @@ export function DOMEventSubscriber(store: Store) {
let lastDate = '';
let wasRunning: boolean;
return function () {
const currentDate = store.getState().ticker.currentDate;
const isRunning = store.getState().ticker.isRunning;
const storeState = store.getState();
const currentDate = selectTickCurrentDate(storeState);
const isRunning = selectTickIsRunning(storeState);
const isFirstDate = selectTickIsFirstDate(storeState);
const isLastDate = selectTickIsLastDate(storeState);

if (currentDate !== lastDate) {
dispatchDOMEvent(store, 'dateChange');
if (store.getState().ticker.isFirstDate) {
if (isFirstDate) {
dispatchDOMEvent(store, 'firstDate');
}
if (store.getState().ticker.isLastDate) {
if (isLastDate) {
dispatchDOMEvent(store, 'lastDate');
}
lastDate = currentDate;
Expand Down
42 changes: 29 additions & 13 deletions src/lib/race.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@ import type { Data, WideData } from './data';
import { createRenderer, rendererSubscriber, type Renderer } from './renderer';
import { createTicker } from './ticker';
import { styleInject } from './styles';
import { actions, createStore, rootReducer, type Store } from './store';
import {
actions,
createStore,
rootReducer,
selectOptions,
selectTickIsRunning,
selectTickDates,
selectTickCurrentDate,
selectTickIsFirstDate,
type Store,
} from './store';
import type { Options } from './options';
import { registerEvents, DOMEventSubscriber, getTickDetails, type EventType } from './events';
import type { Race, ApiCallback } from './models';
Expand Down Expand Up @@ -41,7 +51,8 @@ export async function race(
let preparedData = await prepareData(data, store);
let renderer = createRenderer(preparedData, store, root);

const { injectStyles, theme, autorun } = store.getState().options;
const storeState = store.getState();
const { injectStyles, theme, autorun } = selectOptions(storeState);

const apiSubscriptions: Array<() => void> = [];
subscribeToStore(store, renderer, preparedData);
Expand Down Expand Up @@ -88,7 +99,7 @@ export async function race(
const API = {
// TODO: validate user input
play() {
if (!store.getState().ticker.isRunning) {
if (!selectTickIsRunning(store.getState())) {
ticker.start();
}
},
Expand All @@ -114,13 +125,13 @@ export async function race(
store.dispatch(actions.ticker.updateDate(getDateString(inputDate)));
},
getDate() {
return store.getState().ticker.currentDate;
return selectTickCurrentDate(store.getState());
},
getAllDates() {
return [...store.getState().ticker.dates];
return [...selectTickDates(store.getState())];
},
isRunning() {
return store.getState().ticker.isRunning;
return selectTickIsRunning(store.getState());
},
select(name: string) {
d3.select(root)
Expand Down Expand Up @@ -153,7 +164,8 @@ export async function race(
async changeOptions(newOptions: Partial<Options>) {
const unAllowedOptions: Array<keyof Options> = ['dataShape', 'dataType'];
unAllowedOptions.forEach((key) => {
if (newOptions[key] && newOptions[key] !== store.getState().options[key]) {
const options = selectOptions(store.getState());
if (newOptions[key] && newOptions[key] !== options[key]) {
throw new Error(`The option "${key}" cannot be changed.`);
}
});
Expand All @@ -168,13 +180,14 @@ export async function race(
];
let dataOptionsChanged = false;
dataOptions.forEach((key) => {
if (newOptions[key] && newOptions[key] !== store.getState().options[key]) {
const options = selectOptions(store.getState());
if (newOptions[key] && newOptions[key] !== options[key]) {
dataOptionsChanged = true;
}
});

store.dispatch(actions.options.changeOptions(newOptions));
const { injectStyles, theme, autorun } = store.getState().options;
const { injectStyles, theme, autorun } = selectOptions(store.getState());

if (dataOptionsChanged) {
store.unsubscribeAll();
Expand All @@ -195,7 +208,9 @@ export async function race(
events.reregister();

if (autorun) {
const { isFirstDate, isRunning } = store.getState().ticker;
const state = store.getState();
const isRunning = selectTickIsRunning(state);
const isFirstDate = selectTickIsFirstDate(state);
if (isFirstDate && !isRunning) {
ticker.start();
}
Expand All @@ -205,11 +220,12 @@ export async function race(
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
const currentDate = selectTickCurrentDate(store.getState());
if (currentDate === dateString && dateString !== lastDate) {
lastDate = currentDate; // avoid infinite loop if fn dispatches action
fn.call(API, getTickDetails(store));
}
lastDate = store.getState().ticker.currentDate;
lastDate = currentDate;
});
return {
remove() {
Expand Down
7 changes: 4 additions & 3 deletions src/lib/renderer/calculate-dimensions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as d3 from '../d3';

import type { Data } from '../data';
import type { Store } from '../store';
import { selectOptions, type Store } from '../store';
import { getHeight, getWidth } from '../utils';
import type { RenderOptions } from './render-options';

Expand All @@ -23,10 +23,11 @@ export function calculateDimensions(store: Store, renderOptions: RenderOptions)
labelsWidth,
showIcons,
fixedOrder,
} = store.getState().options;
topN: topNOpt,
} = selectOptions(store.getState());
const { root, maxValue } = renderOptions;

const topN = fixedOrder.length > 0 ? fixedOrder.length : store.getState().options.topN;
const topN = fixedOrder.length > 0 ? fixedOrder.length : topNOpt;

const height = (renderOptions.height = getHeight(root, minHeight, String(inputHeight)));
const width = (renderOptions.width = getWidth(root, minWidth, String(inputWidth)));
Expand Down
33 changes: 18 additions & 15 deletions src/lib/renderer/controls.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import * as d3 from '../d3';

import type { Store } from '../store';
import {
selectOptions,
selectTickIsFirstDate,
selectTickIsLastDate,
selectTickIsRunning,
type Store,
} from '../store';
import { hideElement, showElement } from '../utils';
import { elements } from './elements';
import { buttons } from './buttons';
import type { RenderOptions } from './render-options';

export function renderControls(store: Store, renderOptions: RenderOptions) {
const { marginTop, controlButtons } = store.getState().options;
const { marginTop, controlButtons } = selectOptions(store.getState());
const { root, width, margin, barPadding } = renderOptions;

const elementWidth = root.getBoundingClientRect().width;
Expand Down Expand Up @@ -41,31 +47,28 @@ export function renderControls(store: Store, renderOptions: RenderOptions) {
}

export function updateControls(store: Store, renderOptions: RenderOptions) {
const { overlays, loop } = store.getState().options;
const storeState = store.getState();
const isRunning = selectTickIsRunning(storeState);
const isFirstDate = selectTickIsFirstDate(storeState);
const isLastDate = selectTickIsLastDate(storeState);

const { overlays, loop } = selectOptions(storeState);
const { root } = renderOptions;

if (store.getState().ticker.isRunning) {
if (isRunning) {
showElement(root, elements.pause);
hideElement(root, elements.play);
} else {
showElement(root, elements.play);
hideElement(root, elements.pause);
}

if (
store.getState().ticker.isFirstDate &&
(overlays === 'all' || overlays === 'play') &&
!store.getState().ticker.isRunning
) {
if (isFirstDate && (overlays === 'all' || overlays === 'play') && !isRunning) {
hideElement(root, elements.controls, true);
showElement(root, elements.overlay);
showElement(root, elements.overlayPlay);
hideElement(root, elements.overlayRepeat);
} else if (
store.getState().ticker.isLastDate &&
(overlays === 'all' || overlays === 'repeat') &&
!(loop && store.getState().ticker.isRunning)
) {
} else if (isLastDate && (overlays === 'all' || overlays === 'repeat') && !(loop && isRunning)) {
hideElement(root, elements.controls, true);
showElement(root, elements.overlay);
showElement(root, elements.overlayRepeat);
Expand All @@ -77,7 +80,7 @@ export function updateControls(store: Store, renderOptions: RenderOptions) {
}

export function renderOverlays(store: Store, renderOptions: RenderOptions) {
const { minHeight, minWidth } = store.getState().options;
const { minHeight, minWidth } = selectOptions(store.getState());
const { root } = renderOptions;

const overlayButtonIcons = [
Expand Down
6 changes: 3 additions & 3 deletions src/lib/renderer/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Data } from '../data';
import { actions, type Store } from '../store';
import { actions, selectOptHighlightBars, selectOptSelectBars, type Store } from '../store';
import { safeName, toggleClass, getClicks } from '../utils';
import type { RenderOptions } from './render-options';

Expand Down Expand Up @@ -29,13 +29,13 @@ export function legendClick(ev: MouseEvent, d: string, store: Store) {
}

export function highlightFn(d: Data, store: Store, renderOptions: RenderOptions) {
if (store.getState().options.highlightBars) {
if (selectOptHighlightBars(store.getState())) {
toggleClass(renderOptions.root, 'rect.' + safeName(d.name), 'highlight');
}
}

export function selectFn(d: Data, store: Store, renderOptions: RenderOptions) {
if (store.getState().options.selectBars) {
if (selectOptSelectBars(store.getState())) {
toggleClass(renderOptions.root, 'rect.' + safeName(d.name), 'selected');
store.dispatch(actions.data.toggleSelection(d.name));
}
Expand Down
36 changes: 24 additions & 12 deletions src/lib/renderer/render-frame.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import * as d3 from '../d3';

import type { Data } from '../data';
import type { Store } from '../store';
import {
selectDataGroupFilter,
selectDataSelected,
selectOptShowGroups,
selectOptShowIcons,
selectOptions,
selectTickCurrentDate,
selectTickDates,
type Store,
} from '../store';
import { getDateSlice, safeName, getColor, getIconID, getText } from '../utils';
import type { RenderOptions } from './render-options';
import { selectFn, highlightFn, halo } from './helpers';
Expand All @@ -27,12 +36,11 @@ export function renderFrame(data: Data[], store: Store, renderOptions: RenderOpt
defs,
lastDate,
} = renderOptions;
const dates = store.getState().ticker.dates;
const { showGroups } = store.getState().options;
const storeState = store.getState();
const dates = selectTickDates(storeState);
const showGroups = selectOptShowGroups(storeState);

if (!x) {
return;
}
if (!x) return;

const {
tickDuration,
Expand All @@ -44,17 +52,19 @@ export function renderFrame(data: Data[], store: Store, renderOptions: RenderOpt
fixedScale,
fixedOrder,
labelsPosition,
} = store.getState().options;
topN: topNOpt,
} = selectOptions(storeState);

const topN = fixedOrder.length > 0 ? fixedOrder.length : store.getState().options.topN;
const currentDate = store.getState().ticker.currentDate;
const topN = fixedOrder.length > 0 ? fixedOrder.length : topNOpt;
const currentDate = selectTickCurrentDate(storeState);
const CompleteDateSlice = getDateSlice(currentDate, data, store);
const dateSlice = CompleteDateSlice.slice(0, topN);
const groupFilter = selectDataGroupFilter(storeState);

if (showGroups) {
svg
.selectAll('.legend-wrapper')
.style('opacity', (d: string) => (store.getState().data.groupFilter.includes(d) ? 0.3 : 1));
.style('opacity', (d: string) => (groupFilter.includes(d) ? 0.3 : 1));
}

if (!fixedScale) {
Expand All @@ -72,11 +82,13 @@ export function renderFrame(data: Data[], store: Store, renderOptions: RenderOpt
.selectAll('.bar')
.data(dateSlice, (d: Data) => d.name);

const dataSelected = selectDataSelected(storeState);

bars
.enter()
.append('rect')
.attr('class', (d: Data) => 'bar ' + safeName(d.name))
.classed('selected', (d: Data) => store.getState().data.selected.includes(d.name))
.classed('selected', (d: Data) => dataSelected.includes(d.name))
.attr('x', x(0) + 1)
.attr('width', barWidth)
.attr('y', () => y(topN + 1) + marginBottom + 5)
Expand Down Expand Up @@ -183,7 +195,7 @@ export function renderFrame(data: Data[], store: Store, renderOptions: RenderOpt
.attr('y', () => y(topN + 1) + marginBottom + 5)
.remove();

if (store.getState().options.showIcons) {
if (selectOptShowIcons(store.getState())) {
const iconPatterns = defs.selectAll('.svgpattern').data(dateSlice, (d: Data) => d.name);

iconPatterns
Expand Down
Loading

0 comments on commit 125a1d2

Please sign in to comment.