Skip to content

Commit

Permalink
Make d3 statistic pretty
Browse files Browse the repository at this point in the history
  • Loading branch information
lovephimu committed Jan 28, 2024
1 parent 0ccafd4 commit c9bd1d4
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 59 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ An open chat-room where everyone with an IP and a browser can post!
- While there is mainly the chat interface the possibility of navigating to other pages becomes available on bigger screens - keeping the mobile interface simple and making options available when there is space
- Responsive design: Screen resolution covered: Mobile to 1080p
- Visual feedback: the messaging system is designed to allow only short messages. To keep the user informed there is a character count and additional alerts when the character limit is reached or exceeded
- Although most of the app is styed using Tailwind I do use CSS in special cases like the animated statistics button and the D3 chart

### Todos

Expand Down
166 changes: 114 additions & 52 deletions app/components/ChatUsers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,63 +14,125 @@ export default function ChatUsers(props: Props) {
const [messages, setMessages] = useState<Message[]>([]);

// Fetch functions to read and write Messages

const d3Container = useRef<SVGSVGElement | null>(null);
const containerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (d3Container.current) {
const data = props.agents;

// Calculate max count safely
const maxCount = d3.max(data, (d) => d.count ?? 0) ?? 0;

const margin = { top: 20, right: 20, bottom: 30, left: 40 };
const width = 500 - margin.left - margin.right;
const height = 300 - margin.top - margin.bottom;

const svg = d3
.select(d3Container.current)
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);

// X-axis scale
const x = d3
.scaleBand()
.range([0, width])
.domain(data.map((d) => d.name))
.padding(0.2);

// Y-axis scale
const y = d3.scaleLinear().domain([0, maxCount]).range([height, 0]);

// Add X-axis
svg
.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x));

// Add Y-axis
svg.append('g').call(d3.axisLeft(y));

// Add bars
svg
.selectAll('rect')
.data(data)
.join('rect')
.attr('x', (d) => x(d.name)!)
.attr('y', (d) => y(d.count ?? 0))
.attr('width', x.bandwidth())
.attr('height', (d) => height - y(d.count ?? 0))
.attr('fill', '#69b3a2');
const primaryPink = getComputedStyle(document.documentElement)
.getPropertyValue('--primary-pink')
.trim();

// Function to draw the chart
const drawChart = () => {
if (containerRef.current) {
// Clear any existing content
d3.select(containerRef.current).select('svg').remove();

const data = props.agents;
const maxCount = d3.max(data, (d) => d.count ?? 0) ?? 0;

// Get the dimensions of the container
const containerRect = containerRef.current.getBoundingClientRect();
const width = containerRect.width;
const height = containerRect.height;

const margin = { top: 20, right: 20, bottom: 30, left: 40 };
const effectiveWidth = width - margin.left - margin.right;
const effectiveHeight = height - margin.top - margin.bottom;

// Create the SVG element
const svg = d3
.select(containerRef.current)
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);

const defs = svg.append('defs');

defs
.append('pattern')
.attr('id', 'your-pattern-id') // A unique ID for your pattern
.attr('width', 10) // The width of your pattern tile
.attr('height', 10) // The height of your pattern tile
.attr('patternTransform', 'rotate(45)') // Rotate the pattern
.attr('patternUnits', 'userSpaceOnUse')
.append('path') // Create the cross using path
.attr('d', 'M0,4 L8,4 M4,0 L4,8') // Two lines forming a cross
.attr('stroke', '#ffddd280') // Color of the cross
.attr('stroke-width', 1);

// X-axis scale
const x = d3
.scaleBand()
.range([0, effectiveWidth])
.domain(data.map((d) => d.name))
.padding(0.2);

// Y-axis scale
const y = d3
.scaleLinear()
.domain([0, maxCount])
.range([effectiveHeight, 0]);

// Calculate tick values
const yAxisTicks = y.ticks().filter((tick) => Number.isInteger(tick));

// Add X-axis
svg
.append('g')
.attr('transform', `translate(0,${effectiveHeight})`)
.call(d3.axisBottom(x));

// Add Y-axis
const yAxis = svg
.append('g')
.call(d3.axisLeft(y).tickValues(yAxisTicks));

// yAxis
// .append('text')
// // //.attr('transform', 'rotate(-90)')
// // .attr('y', 0 - margin.left + 20) // Adjust this value as needed
// .attr('x', 30)
// .attr('y', -20)
// .attr('dy', '1em')
// .style('text-anchor', 'middle')
// .text('Number of Messages')
// .style('fill', primaryPink); // Ensure text color is visible

// Add bars
svg
.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', (d) => x(d.name) ?? 0) // Providing a fallback value of 0
.attr('y', (d) => y(d.count) ?? 0)
.attr('width', x.bandwidth())
.attr('height', (d) => effectiveHeight - (y(d.count) ?? 0))
.attr('fill', 'url(#your-pattern-id)')
.attr('stroke', primaryPink) // Color of the border
.attr('stroke-width', 2); // Thickness of the border
}
};

drawChart();

// Optional: Resize observer for dynamic resizing
const resizeObserver = new ResizeObserver(drawChart);
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
}
}, []);

// Cleanup
return () => {
resizeObserver.disconnect();
};
}, [props.agents]);

return (
<div>
<h1>Most active browsers</h1>
<svg ref={d3Container} />
<div ref={containerRef} className="chart-container">
{/* SVG will be appended here by D3 */}
</div>
);
}
5 changes: 3 additions & 2 deletions app/components/NavigationButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import Link from 'next/link';
type Props = {
iconComponent: React.ComponentType;
route: string;
title: string;
};

export default function NavigationButton(props: Props) {
const IconComponent = props.iconComponent;
return (
<Link href={`/${props.route}` as Route}>
<div className="border border-primaryPink m-8 p-2 rounded-xl opacity-0 md:opacity-100 transition duration-500 hover:border-pink-400 statistics-logo">
<Link href={`/${props.route}` as Route} title={props.title}>
<div className="border border-primaryPink m-8 p-2 rounded-xl opacity-0 md:opacity-100 transition duration-500 hover:border-pink-400 statistics-logo max-w-16">
<IconComponent />
</div>
</Link>
Expand Down
13 changes: 13 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
@tailwind components;
@tailwind utilities;

:root {
--primary-pink: #ffddd2;
}

.dynamic-full-height {
height: 100dvh;
}
Expand Down Expand Up @@ -61,3 +65,12 @@
.statistics-logo:hover #Bar3 rect {
height: 90%;
}

.chart-container {
width: 100%; /* Full width of the parent element */
height: 100%; /* Full height of the parent element */
}

.chart-container rect {
color: var(--primary-pink);
}
1 change: 1 addition & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default function RootLayout({
<NavigationButton
route={'visitors'}
iconComponent={StatisticsLogo}
title="Statistics"
/>
</div>
</section>
Expand Down
5 changes: 3 additions & 2 deletions app/visitors/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ export default async function VisitorsPage() {

return (
<section className="text-primaryPink h-full w-full flex justify-center sm:max-w-2xl relative">
<section className="text-primaryPink h-full w-full flex justify-center sm:max-w-2xl sm:border-b-8 sm:border-x sm:rounded-b-2xl sm:border-primaryPink items-center">
<div className="text-primaryPink h-full w-full flex flex-col justify-center sm:max-w-2xl sm:border-b-8 sm:border-x sm:rounded-b-2xl sm:border-primaryPink items-center">
<h3>Three most chatty browsers by message:</h3>
<ChatUsers agents={countedUsers} />
</section>
</div>
</section>
);
}
15 changes: 15 additions & 0 deletions util/__tests__/countUsers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Message } from '@/database/database';
import { expect, test } from '@jest/globals';
import { countUsers } from '../functions/countUsers';

const testInput: Message[] = [
{ id: 293, messageText: 'k', chatUser: 'Mobile Safari @ ::1' },
{ id: 294, messageText: 'k', chatUser: 'Something Else @ ::1' },
{ id: 295, messageText: 'no', chatUser: 'Chrome @ ::1' },
{ id: 296, messageText: 'but', chatUser: 'Safari @ ::1' },
{ id: 297, messageText: 'nooo', chatUser: 'Opera @ ::1' },
];

test('test if only the first three browser are returned', () => {
expect(countUsers(testInput).length).toEqual(3);
});
14 changes: 11 additions & 3 deletions util/functions/countUsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ export type BrowserUsage = {
};

export function countUsers(messages: Message[]): BrowserUsage[] {
messages.forEach((message) => console.log('this is a message'));

// 1. get the actual Names without Ips

const count = messages.map((message) => {
Expand All @@ -28,7 +26,17 @@ export function countUsers(messages: Message[]): BrowserUsage[] {
const result: BrowserUsage[] = Object.keys(countTotals).map((key) => {
return { name: key, count: countTotals[key] };
});
console.log(result);

if (result.length > 3) {
// Set desired length
const desiredLength = Math.min(3, result.length);

result.sort((a, b) => b.count - a.count);
// Create a new array with the desired length
const trimmedArray = result.slice(0, desiredLength);

return trimmedArray;
}

return result;
}

0 comments on commit c9bd1d4

Please sign in to comment.