Skip to content

Commit

Permalink
Extract app state into object
Browse files Browse the repository at this point in the history
  • Loading branch information
onlyskin committed Nov 26, 2023
1 parent a922572 commit fc0d780
Show file tree
Hide file tree
Showing 9 changed files with 367 additions and 292 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
out/
tests/test_runner.bundle.js
src/**/*.js
tests/**/*.js

# Created by https://www.gitignore.io/api/node

Expand Down
1 change: 0 additions & 1 deletion build
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
SRC=src
OUT=out
ESBUILD=node_modules/.bin/esbuild
TSC=node_modules/typescript/bin/tsc

rm -rf $OUT
mkdir -p $OUT
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"name": "pokemon-types-d3",
"version": "1.0.0",
"main": "src/index.ts",
"license": "MIT",
"scripts": {
"build": "./build",
Expand All @@ -15,11 +14,12 @@
"pokeapi-js-wrapper": "^1.1.1"
},
"devDependencies": {
"@types/d3": "^5.0.0",
"@types/d3": "^5.5.0",
"@types/mithril": "^2.2.6",
"@types/node": "^10.5.2",
"@types/ospec": "^4.0.10",
"esbuild": "^0.19.7",
"ospec": "^4.2.0",
"tsc": "^2.0.4",
"typescript": "^5.3.2"
}
}
46 changes: 33 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,48 @@
import m from 'mithril';
import stream from 'mithril/stream';
import * as d3 from 'd3';
import { PokemonType, INode } from './type_to_nodes';
import { updateVisualisation } from './update_visualisation';
import { focusedType, hoveredNode, visualisationTitle } from './utils';
import { IState } from './utils';
import { forceSimulation } from './simulation';

const state: IState = {
focusedType: stream<PokemonType>('fire'),
hoveredNode: stream<INode | undefined>(undefined),
setFocusedType: function(newType: PokemonType) {
if (newType === this.focusedType()) {
return;
}

this.focusedType(newType);
m.redraw();
},
setHoveredNode: function(newNode?: INode) {
if (newNode === this.hoveredNode()) {
return;
}

this.hoveredNode(newNode);
m.redraw();
}
};

const Visualisation: m.Component<{
focused: PokemonType,
title: string,
simulation: d3.Simulation<INode, undefined>,
state: IState,
}, {
oldFocused: PokemonType,
}> = {
oncreate: ({attrs: {focused, title, simulation}, dom}) => {
updateVisualisation(dom, focused, title, simulation, true);
this.oldFocused = focused;
oncreate: function({attrs: {simulation, state}, dom}) {
updateVisualisation(dom, simulation, true, state);
this.oldFocused = state.focusedType();
},
onupdate: ({attrs: {focused, title, simulation}, dom}) => {
const focusedUpdated = focused !== this.oldFocused;
updateVisualisation(dom, focused, title, simulation, focusedUpdated);
this.oldFocused = focused;
onupdate: function({attrs: {simulation, state}, dom}) {
const focusedUpdated = state.focusedType() !== this.oldFocused;
updateVisualisation(dom, simulation, focusedUpdated, state);
this.oldFocused = state.focusedType();
},
view: ({attrs: {focused}}) => {
view: () => {
return m(
'svg',
{
Expand All @@ -41,8 +62,7 @@ const simulation = forceSimulation();
m.mount(document.body, {
view: () => {
return m(Visualisation, {
focused: focusedType(),
title: visualisationTitle(hoveredNode(), focusedType()),
state,
simulation,
});
}
Expand Down
4 changes: 0 additions & 4 deletions src/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
declare module 'ospec';
declare module 'mithril-query';
declare module 'mithril/test-utils/browserMock';
declare module 'mithril-stream';
declare module 'pokeapi-js-wrapper';
59 changes: 40 additions & 19 deletions src/update_visualisation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as d3 from 'd3';
import { Pokedex } from 'pokeapi-js-wrapper';
import { PokemonType, INode } from './type_to_nodes';
import { boundingDimensions, focusedType, updateFocusedType, updateHoveredNode, nodeRadius } from './utils';
import { boundingDimensions, nodeRadius, IState } from './utils';
import type_to_nodes from './type_to_nodes';
import { tick } from './simulation';

Expand All @@ -23,38 +23,58 @@ function preloadData(nodes: INode[]): void {

export async function updateVisualisation(
svg: Element,
focused: PokemonType,
title: string,
simulation: d3.Simulation<INode, undefined>,
focusedUpdated: boolean,
state: IState,
): Promise<void> {
const { width, height } = boundingDimensions(svg);
svg.setAttribute('width', width.toString());
svg.setAttribute('height', height.toString());

updateTitle(svg, title);
updateTitle(svg, state);

if (focusedUpdated) {
const response = await pokedex.getTypeByName(focused);
const response = await pokedex.getTypeByName(state.focusedType());
const nodes: INode[] = type_to_nodes(response);
preloadData(nodes);

simulation.nodes(nodes);
tick(simulation);

}

updateCircles(svg, simulation, focusedUpdated);
updateFocused(svg, focused);
updateCircles(svg, simulation, state);
updateFocused(svg, state.focusedType());
}


function updateFocused(svg: Element, focused: PokemonType): void {
updateTextElement(svg, (focused as string), 'focused', 0.5, 0.5);
}

function updateTitle(svg: Element, title: string): void {
updateTextElement(svg, title, 'title', 0.5, 0.125);
export function visualisationTitle(state: IState): string {
const hovered = state.hoveredNode();
const focused = state.focusedType();

if (hovered === undefined) {
return '';
}

let attacking;
let defending;

if (hovered.direction === 'from') {
attacking = hovered.name;
defending = focused;
} else {
attacking = focused;
defending = hovered.name;
}

return `${attacking} gets ${hovered.multiplier}x against ${defending}`;
}

function updateTitle(svg: Element, state: IState): void {
updateTextElement(svg, visualisationTitle(state), 'title', 0.5, 0.125);
}

function updateTextElement(
Expand All @@ -67,12 +87,12 @@ function updateTextElement(
const { width, height } = boundingDimensions(svg);

const updating = d3.select(svg)
.selectAll(`.${className}`)
.selectAll<Element, string>(`.${className}`)
.data<string>([text], d => (d as string));

updating
.enter()
.append('text')
.append<Element>('text')
.merge(updating)
.classed(className, true)
.attr('x', width * xMultiple)
Expand All @@ -87,11 +107,11 @@ function updateTextElement(
function updateCircles(
svg: Element,
simulation: d3.Simulation<INode, undefined>,
focusedUpdated: boolean
state: IState,
): void {
const { width, height } = boundingDimensions(svg);
const nodeTransition = d3.transition()
.duration(focusedUpdated ? 600 : 0);
.duration(600);

const updatingNodes = d3.select(svg)
.selectAll<Element, INode>('circle')
Expand All @@ -114,23 +134,24 @@ function updateCircles(
.attr('cx', d => d.x * width)
.attr('cy', d => d.y * height)
.on('click', function(d) {
if (focusedType() === d.name) {
if (state.focusedType() === d.name) {
return;
}

updateFocusedType(d.name);
updateHoveredNode(undefined);
state.setFocusedType(d.name);
state.setHoveredNode(undefined);
this.dispatchEvent(new Event('mouseout'));
})
.on('mouseover', d => updateHoveredNode(d))
.on('mouseout', d => updateHoveredNode(undefined))
.on('mouseover', d => state.setHoveredNode(d))
.on('mouseout', d => state.setHoveredNode(undefined))
.attr('r', 0);

mergedNodes
.transition(nodeTransition)
.attr('cx', d => d.x * width)
.attr('cy', d => d.y * height)
.attr('r', d => nodeRadius(d) * Math.min(width, height));

exitingNodes
.transition(nodeTransition)
.attr('r', 0)
Expand Down
48 changes: 6 additions & 42 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,18 @@
import stream from 'mithril/stream';
import m from 'mithril';
import { PokemonType, INode } from './type_to_nodes';

export const focusedType = stream<PokemonType>('fire');

export const hoveredNode = stream<INode | undefined>(undefined);
export interface IState {
focusedType: () => PokemonType;
hoveredNode: () => INode | undefined;
setFocusedType: (newType: PokemonType) => undefined;
setHoveredNode: (newNode?: INode) => undefined;
}

const NODE_SIZE_FACTOR = 0.03;

export function nodeRadius(node: INode): number {
return (node.multiplier === 0 ? 0.1 : node.multiplier) * NODE_SIZE_FACTOR;
}

export function updateFocusedType(newType: PokemonType) {
if (newType === focusedType()) {
return;
}

focusedType(newType);
m.redraw();
}

export function updateHoveredNode(newNode?: INode) {
if (newNode === hoveredNode()) {
return;
}

hoveredNode(newNode);
m.redraw();
}

export function boundingDimensions(svg: Element) {
const boundingRect = (svg.getBoundingClientRect() as DOMRect);
const width = boundingRect.width;
Expand All @@ -40,22 +23,3 @@ export function boundingDimensions(svg: Element) {
export function boundingWidth(svg: Element) {
return boundingDimensions(svg).width;
}

export function visualisationTitle(hovered: INode, focused: PokemonType): string {
if (hovered === undefined) {
return '';
}

let attacking;
let defending;

if (hovered.direction === 'from') {
attacking = hovered.name;
defending = focused;
} else {
attacking = focused;
defending = hovered.name;
}

return `${attacking} gets ${hovered.multiplier}x against ${defending}`;
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"include": ["src/**/*", "tests/**/*"],
"compilerOptions": {
"esModuleInterop": true,
"noImplicitAny": true,
"noUnusedLocals": true,
"downlevelIteration": true,
"target": "es5",
"lib": [
"es6", "dom"
Expand Down
Loading

0 comments on commit fc0d780

Please sign in to comment.