diff --git a/src/features/statistics/StatisticsUtils.ts b/src/features/statistics/StatisticsUtils.ts index a961edc..855784a 100644 --- a/src/features/statistics/StatisticsUtils.ts +++ b/src/features/statistics/StatisticsUtils.ts @@ -34,7 +34,7 @@ export const normalizeArray = (array: number[]): number[] => { }; export interface MovieStatistics { - movieTitle: string; + title: string; dateWatched: string; vote_average: number; revenue: number; @@ -156,7 +156,7 @@ export const loadDefaultChartSettings = (params: { showInLegend: false, tooltip: { renderer: (params: AgScatterSeriesTooltipRendererParams) => - `
${params.datum.movieTitle}
` + + `
${params.datum.title}
` + `
${params.xName}: ${params.xValue}
${params.yName}: ${params.yValue}
`, }, }, diff --git a/src/features/statistics/StatisticsView.vue b/src/features/statistics/StatisticsView.vue index dbb2f29..bd4f9ad 100644 --- a/src/features/statistics/StatisticsView.vue +++ b/src/features/statistics/StatisticsView.vue @@ -7,16 +7,37 @@ /> - {{ normButtonText }} - -

- Normalizing scores adjusts each member's ratings to account for their different scoring patterns. - A normalized score of 0 means average, while lower and higher values indicate scores below and above your usual rating. -

+
+
+
+ + +
+
+ {{ normButtonText }} +
+ Normalizing scores adjusts each member's ratings to account for their different scoring patterns. + A normalized score of 0 means average, while lower and higher values indicate scores below and above your usual rating. +
+
+
+

@@ -48,24 +69,7 @@

--> - - - - - - +
@@ -74,7 +78,18 @@ import { AgHistogramSeriesTooltipRendererParams, AgBarSeriesTooltipRendererParams } from "ag-charts-community"; import { AgChartsVue } from "ag-charts-vue3"; import { DateTime } from "luxon"; -import { ref, computed, watch } from "vue"; +import { ref, computed, watch, h } from "vue"; +import { filterMovies } from '@/common/searchMovies'; +import VAvatar from "@/common/components/VAvatar.vue"; +import AverageImg from "@/assets/images/average.svg"; +import MovieTooltip from "@/features/reviews/components/MovieTooltip.vue"; +import { + createColumnHelper, + getCoreRowModel, + getSortedRowModel, + useVueTable, +} from "@tanstack/vue-table"; +import TableView from "@/features/reviews/components/TableView.vue"; import { normalizeArray, @@ -135,12 +150,20 @@ const loading = computed( loadingCalculations.value, ); +const searchTerm = ref(""); +const searchInput = ref(null); +const showTooltip = ref(false); + +const filteredMovieData = computed(() => { + return filterMovies(movieData.value, searchTerm.value); +}); + const fetchMovieData = (reviews: DetailedReviewListItem[]) => { return reviews.map((review) => { if (!review.externalData) return null; return { - movieTitle: review.title, + title: review.title, dateWatched: DateTime.fromISO(review.createdDate).toLocaleString(), ...Object.keys(review.scores).reduce>( (acc, key) => { @@ -150,6 +173,8 @@ const fetchMovieData = (reviews: DetailedReviewListItem[]) => { {}, ), // Map the new external data structure + imageUrl: review.imageUrl, + createdDate: review.createdDate, vote_average: review.externalData.vote_average, revenue: review.externalData.revenue, budget: review.externalData.budget, @@ -157,6 +182,7 @@ const fetchMovieData = (reviews: DetailedReviewListItem[]) => { genres: review.externalData.genres, production_companies: review.externalData.production_companies, production_countries: review.externalData.production_countries, + externalData: review.externalData, }; }).filter(Boolean); // Remove any null entries }; @@ -257,11 +283,40 @@ watch(selectedChartBase, generateCustomChart); const loadChartOptions = async () => { try { chartLoadingStates.value.histogram = true; + // Filter the histogram data based on the filtered movies + const filteredHistData = histogramData.value.map(bin => { + const filtered = { ...bin }; + members.value.forEach(member => { + filtered[member.id] = 0; + }); + return filtered; + }); + const filteredHistNormData = histogramNormData.value.map(bin => { + const filtered = { ...bin }; + members.value.forEach(member => { + filtered[member.id] = 0; + }); + return filtered; + }); + + // Populate the filtered histogram data + filteredMovieData.value.forEach(movie => { + members.value.forEach(member => { + const score = Math.floor(movie[member.id]); + if (!isNaN(score)) { + filteredHistData[score][member.id] += 1; + } + let scoreNorm = Math.floor(movie[member.id + "Norm"] * 4 + 5); + scoreNorm = scoreNorm < 0 ? 0 : scoreNorm > 10 ? 10 : scoreNorm; + filteredHistNormData[scoreNorm][member.id] += 1; + }); + }); + histChartOptions.value = { autoSize: true, theme: "ag-default-dark", title: { text: "Score Histogram" }, - data: normalize.value ? histogramNormData.value : histogramData.value, + data: normalize.value ? filteredHistNormData : filteredHistData, series: members.value.map((member) => { return { type: "line", @@ -315,7 +370,7 @@ const loadChartOptions = async () => { chartLoadingStates.value.histogram = false; // Special handling for TMDB score chart where TMDB score is available - const validTMDBData = movieData.value.filter(movie => + const validTMDBData = filteredMovieData.value.filter(movie => movie.vote_average && movie.vote_average > 0 && movie.average && movie.average > 0 ); @@ -341,7 +396,7 @@ const loadChartOptions = async () => { yData: "average", normalizeY: true, normalizeToggled: normalize.value, - movieData: movieData.value, + movieData: filteredMovieData.value, }); revenueChartOptions.value = loadDefaultChartSettings({ @@ -353,7 +408,7 @@ const loadChartOptions = async () => { yData: "average", normalizeY: true, normalizeToggled: normalize.value, - movieData: movieData.value, + movieData: filteredMovieData.value, }); dateChartOptions.value = loadDefaultChartSettings({ @@ -365,7 +420,7 @@ const loadChartOptions = async () => { yData: "average", normalizeY: true, normalizeToggled: normalize.value, - movieData: movieData.value, + movieData: filteredMovieData.value, }); genreChartOptions.value = generateGenreChart(); @@ -454,7 +509,7 @@ const normName = (name = "average") => { const headers = computed(() => { const headers: Header[] = [ - { value: "movieTitle", style: "font-bold", title: "Title" }, + { value: "title", style: "font-bold", title: "Title" }, { value: "dateWatched", title: "Date Reviewed" }, ]; @@ -470,4 +525,74 @@ const headers = computed(() => { }); return headers; }); + +// Add a watch on filteredMovieData to trigger chart updates +watch(filteredMovieData, () => { + if (filteredMovieData.value) { + loadChartOptions(); + } +}, { immediate: true }); + +const columnHelper = createColumnHelper(); + +const columns = computed(() => [ + columnHelper.accessor("title", { + header: "Title", + cell: (info) => h(MovieTooltip, { + title: info.getValue(), + imageUrl: info.row.original.imageUrl, + movie: info.row.original.externalData + }), + meta: { + class: "font-bold", + }, + }), + columnHelper.accessor("dateWatched", { + header: "Date Reviewed", + }), + ...members.value.map((member) => + columnHelper.accessor(normName(member.id), { + id: normName(member.id), + header: () => h(VAvatar, { + src: member.image, + name: member.name, + }), + cell: (info) => { + const value = info.getValue(); + return value !== undefined ? Math.round(value * 100) / 100 : ''; + }, + sortUndefined: "last", + }), + ), + columnHelper.accessor(normName(), { + header: () => h("img", { + src: AverageImg, + class: "h-12 w-16 max-w-none" + }), + cell: (info) => { + const value = info.getValue(); + return value !== undefined ? Math.round(value * 100) / 100 : ''; + }, + sortUndefined: "last", + }), + columnHelper.accessor("vote_average", { + header: "TMDB", + cell: (info) => { + const value = info.getValue(); + return value !== undefined ? Math.round(value * 100) / 100 : ''; + }, + }), +]); + +const movieTable = useVueTable({ + get columns() { + return columns.value; + }, + get data() { + return filteredMovieData.value ?? []; + }, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getRowId: (row) => row.title, +});