Skip to content

Commit

Permalink
♻️(frontend) refactor Views plotting component
Browse files Browse the repository at this point in the history
Component is now subscribing to filters state.
Note: Time management in the current version of
the frontend and the API is still bulky. A lot
of work need to be done.
  • Loading branch information
lebaudantoine committed Jul 20, 2023
1 parent 5244ac6 commit cba757e
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 79 deletions.
151 changes: 72 additions & 79 deletions src/frontend/packages/ui/video/Views.tsx
Original file line number Diff line number Diff line change
@@ -1,101 +1,94 @@
import React, { useState, useEffect } from "react";
import React, { useMemo } from "react";
import ReactECharts from "echarts-for-react";
import type { EChartsOption } from "echarts-for-react";
import { useQuery, useQueries, useQueryClient } from "@tanstack/react-query";

import cloneDeep from "lodash.clonedeep";
import { axios } from "../libs/axios";
import { VideoViewsResponse } from "./types";
import useFilters from "../hooks/useFilters";
import { useVideosViews } from "./api/getVideoViews";

type DailyViewsResponseItem = {
day: string;
views: number;
type Series = {
id: string;
name: string;
data: number[];
type: string;
smooth: number;
symbol: string;
emphasis: any;
};

type VideoViewsResponse = {
total: number;
daily_views: Array<DailyViewsResponseItem>;
};

export const DailyViews = () => {
const { videoIds } = useFilters();

const baseOption: EChartsOption = {
grid: { top: 80, right: 8, bottom: 100, left: 50 },
xAxis: {
type: "category",
data: [],
axisTick: {
alignWithLabel: true,
interval: 0,
},
axisLabel: {
interval: 0,
rotate: 45,
height: 200,
},
},
yAxis: {
type: "value",
name: "# views",
},
series: [],
tooltip: {
trigger: "axis",
const baseOption: EChartsOption = {
grid: { top: 80, right: 8, bottom: 100, left: 50 },
xAxis: {
type: "category",
data: [],
axisTick: {
alignWithLabel: true,
interval: 0,
},
textStyle: {
fontFamily: "Roboto, sans-serif",
axisLabel: {
interval: 0,
rotate: 45,
height: 200,
},
};

const [option, setOption] = useState(baseOption);

const newOption = cloneDeep(option);
},
yAxis: {
type: "value",
name: "# views",
min: 0,
max: 100,
},
series: [],
tooltip: {
trigger: "axis",
},
textStyle: {
fontFamily: "Roboto, sans-serif",
},
};

function getVideoViews(videoId: string) {
return axios.get(`video/${videoId}/views`).then((res) => res.data);
}
export const DailyViews: React.FC = () => {
const {
date: [since, until],
videoIds,
} = useFilters();

function addOneSeries(
videoId: string,
daily_views: Array<DailyViewsResponseItem>
) {
newOption.xAxis.data = daily_views.map((d) => d.day);
newOption.series.push({
name: videoId,
data: daily_views.map((d) => d.views),
type: "line",
smooth: 0.2,
symbol: "none",
areaStyle: {},
stack: "Total",
emphasis: {
focus: "series",
},
});
setOption(newOption);
}
const validData = useVideosViews({ videoIds, since, until });

const results = useQueries({
queries: videoIds.map((videoId) => {
return {
queryKey: [`videoViews-${videoId}`],
queryFn: () => getVideoViews(videoId),
onSuccess: (data: VideoViewsResponse) => {
addOneSeries(videoId, data.daily_views);
return data;
},
};
}),
const parseSeries = (item: VideoViewsResponse): Series => ({
id: item?.id,
name: item.id.slice(-5) || "",
data: item.count_by_date.map((day) => day.count) || [],
type: "line",
smooth: 0.2,
symbol: "none",
emphasis: {
focus: "series",
},
});

if (results.some((result) => result.isLoading))
return <span>Loading...</span>;
const parseXAxis = (item: VideoViewsResponse): Array<string> =>
item.count_by_date.map((day) => day.date) || [];

const formattedOption = useMemo(() => {
if (!validData.length) {
return baseOption;
}
const newOption = cloneDeep(baseOption);
// todo - ongoing discussion in the team, we assume all requests share the same xAxis.
newOption.xAxis.data = parseXAxis(validData[0]);
newOption.series = validData.map((d) => parseSeries(d));
return newOption;
}, [validData]);

return (
<>
<div className="chart-title">Video: daily views</div>
<ReactECharts option={option} style={{ height: 500 }} />
<ReactECharts
option={formattedOption}
notMerge={true} // todo - update it with replaceMerge, an issue is opened.
style={{ height: 500 }}
/>
</>
);
};
47 changes: 47 additions & 0 deletions src/frontend/packages/ui/video/api/getVideoViews.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useQueries } from "@tanstack/react-query";
import { axios } from "../../libs/axios";
import { VideoViewsResponse } from "../types";

const getVideoViews = async (
videoId: string,
since: string,
until: string
): Promise<VideoViewsResponse> => {
const response = await axios.get(`video/${videoId}/views`, {
params: {
...(since && { since: since }),
...(until && { until: until }),
},
});
return {
id: videoId,
...response?.data?.content,
};
};

type UseVideosViewsOptions = {
videoIds: Array<string>;
since: string;
until: string;
};

export const useVideosViews = ({
videoIds,
since,
until,
}: UseVideosViewsOptions): VideoViewsResponse[] => {
// Generate the queries array
const queries = videoIds?.map((videoId) => ({
queryKey: ["videoViews", videoId, since, until],
queryFn: () => getVideoViews(videoId, since, until),
}));

// Use useQueries hook to fetch data for all videoIds in parallel
const queryResults = useQueries({ queries });

// todo - add checks to make sure data have the same number of day's count.
// Extract the data from the query results
return queryResults
.filter((r) => r.isSuccess)
.map((queryResult) => queryResult.data) as VideoViewsResponse[];
};
10 changes: 10 additions & 0 deletions src/frontend/packages/ui/video/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type VideoViewsResponseItem = {
date: string;
count: number;
};

export type VideoViewsResponse = {
id: string;
total_views: number;
count_by_date: Array<VideoViewsResponseItem>;
};

0 comments on commit cba757e

Please sign in to comment.