Skip to content

Commit

Permalink
Feature: Add EE for topics panel (#861)
Browse files Browse the repository at this point in the history
* Add header section with button, slider, add table tray with tabs and resizable component

* Add epoch table and more tabs

* Move components and reorganize

* Setup ee for PA

* Add PA tables to tray config

* Use half pointers for slider

* Use props for tab state

* Move active tab state a level up

* Add callbacks to retrieve vertical bar color hash

* Add query state

* Change colors for dark mode

* Add checkboxes for PA

* Change checkboxes layout

* Setup topics animation

* Break p5 into files and use with React

* Draw info box

* Update container sizes

* overflow to resize

* Handle play/pause and reset callbacks

* Handle animation speed step

* Handle user visit data change

* Render data in the tables

* UI changes

* Render border conditionally

* Update border

* Animation, table UI changes

* Refactor code

* Update UI, handle unexpected flow by updating calls

* Auto change active tab

* Extend timeline

* Make animation continuous with animation consitency if moved to previous tab

* Refactor callback and conditionally call user visit

* use ref for not rendering on animation var value change

* Dont overlap with horizontal lines

* Update image data

* Add line  from circle to infoox

* Update color circles sizes and text placement

* handle restart of animation on complete when replay is clicked

* Update callbacks for user visit
  • Loading branch information
mayan-000 authored and amovar18 committed Dec 12, 2024
1 parent f8ba7f9 commit 100b4d4
Show file tree
Hide file tree
Showing 13 changed files with 1,159 additions and 477 deletions.
674 changes: 250 additions & 424 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion packages/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@
"classnames": "^2.3.2",
"d3": "^7.9.0",
"file-saver": "^2.0.5",
"lodash-es": "^4.17.21",
"github-markdown-css": "^5.6.1",
"lodash-es": "^4.17.21",
"marked": "^14.0.0",
"marked-alert": "^2.0.2",
"mermaid": "^11.2.0",
"p-queue": "^7.3.4",
"p5": "^1.11.0",
"pretty-print-json": "^3.0.2",
"re-resizable": "^6.9.9",
"react": "^18.2.0",
Expand All @@ -54,6 +55,7 @@
"victory": "^36.6.11"
},
"devDependencies": {
"@types/p5": "^1.7.6",
"@types/react-copy-to-clipboard": "^5.0.4",
"devtools-protocol": "^0.0.1345247",
"html-inline-script-webpack-plugin": "^3.2.1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import Slider from './slider';

interface HeaderProps {
play: boolean;
setPlay: React.Dispatch<React.SetStateAction<boolean>>;
setPlay: () => void;
reset: () => void;
historyCount: number;
sliderStep: number;
Expand All @@ -49,7 +49,7 @@ const Header = ({
<div className="flex items-center divide-x divide-gray-300 dark:divide-bright-gray text-slate-700 dark:text-bright-gray">
<button
className="pr-2"
onClick={() => setPlay(!play)}
onClick={setPlay}
title={play ? 'Pause' : 'Play'}
>
{play ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ interface TableTrayProps {

const TableTray = ({ tabItems, activeTab, setActiveTab }: TableTrayProps) => {
const ActiveTabContent = tabItems[activeTab].content.Element;
const props = tabItems[activeTab].content.props;

return (
<div className="w-full h-full">
Expand All @@ -40,7 +41,7 @@ const TableTray = ({ tabItems, activeTab, setActiveTab }: TableTrayProps) => {
fontSizeClass="text-xs"
/>
</div>
<ActiveTabContent />
<ActiveTabContent {...props} />
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* External dependencies.
*/
import React, { useEffect, useRef, useState } from 'react';
import p5 from 'p5';

/**
* Internal dependencies.
*/
import { topicsAnimation } from './topicsAnimation';

interface AnimationProps {
epoch: { datetime: string; website: string; topics: string[] }[];
isAnimating: boolean;
siteAdTechs: Record<string, string[]>;
handleUserVisit: (visitIndex: number, updateTopics?: boolean) => void;
isPlaying: boolean;
resetAnimation: boolean;
speedMultiplier: number;
}

const Animation = ({
epoch,
isAnimating,
siteAdTechs,
handleUserVisit,
isPlaying,
resetAnimation,
speedMultiplier,
}: AnimationProps) => {
const node = useRef(null);
const [togglePlayCallback, setTogglePlayCallback] =
useState<(state: boolean) => void>();
const [resetCallback, setResetCallback] = useState<() => void>();
const [speedMultiplierCallback, setSpeedMultiplierCallback] =
useState<(speed: number) => void>();
const animationRef = useRef(isAnimating);

useEffect(() => {
// Using the useRef hook to store the current value of isAnimating because the animation should not be re-rendered when the value of isAnimating changes.
animationRef.current = isAnimating;
}, [isAnimating]);

useEffect(() => {
const tAnimation = (p: p5) => {
const { togglePlay, reset, updateSpeedMultiplier } = topicsAnimation(
p,
epoch,
animationRef.current,
siteAdTechs,
animationRef.current
? handleUserVisit
: (idx: number) => handleUserVisit(idx, false)
);

setTogglePlayCallback(() => togglePlay);
setResetCallback(() => reset);
setSpeedMultiplierCallback(() => updateSpeedMultiplier);
};

const p = node.current ? new p5(tAnimation, node.current) : null;

return () => {
p?.remove();
};
}, [epoch, handleUserVisit, siteAdTechs]);

useEffect(() => {
togglePlayCallback?.(isPlaying);
}, [isPlaying, togglePlayCallback]);

useEffect(() => {
if (resetAnimation) {
resetCallback?.();
}
}, [resetAnimation, resetCallback]);

useEffect(() => {
speedMultiplierCallback?.(speedMultiplier);
}, [speedMultiplier, speedMultiplierCallback]);

return <div ref={node} className="overflow-auto" />;
};

export default Animation;
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,166 @@
/**
* External dependencies.
*/
import React, { useState } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { Resizable } from 're-resizable';
import { noop } from '@google-psat/common';

/**
* Internal dependencies.
*/
import Header from '../../../explorableExplanation/header';
import Tray from './tray';
import Animation from './animation';
import { assignAdtechsToSites, createEpochs } from './topicsAnimation/utils';
import type { TopicsTableType } from './topicsTable';
import { adtechs, websites } from './topicsAnimation/data';

const ExplorableExplanation = () => {
const [play, setPlay] = useState(false);
const [play, setPlay] = useState(true);
const [reset, _setReset] = useState(false);
const [sliderStep, setSliderStep] = useState(1);
const historyCount = 10;
const [activeTab, setActiveTab] = useState(0);
const [topicsTableData, setTopicsTableData] = useState<
Record<number, TopicsTableType[]>
>({});
const [epochCompleted, setEpochCompleted] = useState<Record<number, boolean>>(
{}
);
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

const epochs = useMemo(() => createEpochs(), []);
const siteAdTechs = useMemo(() => {
return assignAdtechsToSites(websites, adtechs);
}, []);

const setReset = useCallback(() => {
setPlay(false);
_setReset(true);
setSliderStep(1);
setTopicsTableData({});
setActiveTab(0);
setEpochCompleted({});

timeoutRef.current = setTimeout(() => {
_setReset(false);
setPlay(true);
}, 0);
}, []);

useEffect(() => {
if (!epochCompleted[activeTab]) {
setTopicsTableData((prevTopicsTableData) => {
const newTopicsTableData = { ...prevTopicsTableData };
newTopicsTableData[activeTab] = [];
return newTopicsTableData;
});
}
}, [activeTab, epochCompleted]);

useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);

const handleTopicsCalculation = useCallback(
(visitIndex: number) => {
setTopicsTableData((prevTopicsTableData) => {
const { topics, website } = epochs[activeTab].webVisits[visitIndex];
const topicsData = prevTopicsTableData[activeTab];
const newTopicsTableData = {
...prevTopicsTableData,
};

const newTopicsData = topics.reduce(
(acc, topic) => {
const existingTopic = acc.find((t) => t.topicName === topic);
if (existingTopic) {
existingTopic.count += 1;
existingTopic.observedByContextDomains = [
...new Set([
...existingTopic.observedByContextDomains,
...siteAdTechs[website],
]),
];
} else {
acc.push({
topicName: topic,
count: 1,
observedByContextDomains: siteAdTechs[website] || [],
});
}
return acc;
},
[...(topicsData || [])]
);

newTopicsTableData[activeTab] = newTopicsData;

return newTopicsTableData;
});
},
[activeTab, epochs, siteAdTechs]
);

const handleUserVisit = useCallback(
(visitIndex: number, updateTopics = true) => {
if (visitIndex < epochs[activeTab].webVisits.length && updateTopics) {
handleTopicsCalculation(visitIndex);
}

if (visitIndex === epochs[activeTab].webVisits.length) {
if (activeTab < 3 && updateTopics) {
setActiveTab(activeTab + 1);
} else {
setPlay(false);
}

setEpochCompleted((prevEpochCompleted) => ({
...prevEpochCompleted,
[activeTab]: true,
}));
}
},
[activeTab, epochs, handleTopicsCalculation]
);

const handlePlay = useCallback(() => {
if (activeTab === 3 && epochCompleted[activeTab]) {
setReset();
} else {
setPlay((prevPlay) => !prevPlay);
}
}, [activeTab, epochCompleted, setReset]);

return (
<div className="flex flex-col h-full">
<Header
play={play}
setPlay={setPlay}
setPlay={handlePlay}
sliderStep={sliderStep}
setSliderStep={setSliderStep}
historyCount={historyCount}
reset={noop}
historyCount={epochs[activeTab].webVisits.length}
reset={setReset}
/>
<div className="flex-1" />
<div className="flex-1 overflow-auto">
<Animation
epoch={epochs[activeTab].webVisits}
isAnimating={!epochCompleted?.[activeTab]}
siteAdTechs={siteAdTechs}
isPlaying={play}
resetAnimation={reset}
speedMultiplier={sliderStep}
handleUserVisit={handleUserVisit}
/>
</div>
<Resizable
defaultSize={{
width: '100%',
Expand All @@ -55,7 +189,11 @@ const ExplorableExplanation = () => {
}}
className="h-full flex"
>
<Tray />
<Tray
activeTab={activeTab}
setActiveTab={setActiveTab}
topicsTableData={topicsTableData[activeTab] || []}
/>
</Resizable>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export const config = {
canvas: {
height: 500,
},
timeline: {
position: { x: 0, y: 120 },
circleProps: {
diameter: 50,
horizontalSpacing: 150,
},
stepDelay: 1500,
user: {
width: 30,
height: 30,
},
circles: [],
},
};
Loading

0 comments on commit 100b4d4

Please sign in to comment.