diff --git a/Rents_Only.postman_collection.json b/Rents_Only.postman_collection.json index aa26f91..f3fe7bf 100644 --- a/Rents_Only.postman_collection.json +++ b/Rents_Only.postman_collection.json @@ -202,3 +202,4 @@ + diff --git a/Rents_Only_Paginated.postman_collection.json b/Rents_Only_Paginated.postman_collection.json index 47c9e52..dfdcf3a 100644 --- a/Rents_Only_Paginated.postman_collection.json +++ b/Rents_Only_Paginated.postman_collection.json @@ -153,3 +153,4 @@ + diff --git a/public/js/index.js b/public/js/index.js index 70ec75c..f4269a6 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -181,3 +181,4 @@ document.addEventListener('DOMContentLoaded', () => { + diff --git a/public/rents.html b/public/rents.html index ef3646f..6f464e6 100644 --- a/public/rents.html +++ b/public/rents.html @@ -499,3 +499,4 @@ + diff --git a/src/services/chartFormatter.js b/src/services/chartFormatter.js index 05a307e..5a21107 100644 --- a/src/services/chartFormatter.js +++ b/src/services/chartFormatter.js @@ -155,11 +155,54 @@ class ChartFormatter { return visualizations; } + generateLineChartSummary(chart, data, parsedQuery) { + const { labels, datasets } = chart.data; + const values = datasets[0]?.data || []; + + if (values.length === 0 || values.every(v => v === null || v === undefined)) { + return "No data available for this time period."; + } + + const validValues = values.filter(v => v !== null && v !== undefined && !isNaN(v)); + if (validValues.length === 0) { + return "No valid data points available for this chart."; + } + + const min = Math.min(...validValues); + const max = Math.max(...validValues); + const avg = Math.round(validValues.reduce((a, b) => a + b, 0) / validValues.length); + const firstValue = validValues[0]; + const lastValue = validValues[validValues.length - 1]; + const trend = lastValue > firstValue ? 'increased' : lastValue < firstValue ? 'decreased' : 'remained stable'; + const changePercent = firstValue > 0 ? Math.round(((lastValue - firstValue) / firstValue) * 100) : 0; + + const areaText = parsedQuery.areas && parsedQuery.areas.length > 0 + ? parsedQuery.areas.join(' and ') + : 'the selected areas'; + const timeText = parsedQuery.time_period + ? `over the last ${parsedQuery.time_period.count} ${parsedQuery.time_period.type}` + : 'over the selected period'; + + let summary = `The line chart shows rental price trends for ${areaText} ${timeText}. `; + summary += `Prices ranged from ${this.formatCurrency(min)} to ${this.formatCurrency(max)}, `; + summary += `with an average of ${this.formatCurrency(avg)}. `; + + if (changePercent !== 0) { + summary += `Overall, prices ${trend} by ${Math.abs(changePercent)}% `; + summary += `from ${this.formatCurrency(firstValue)} in ${labels[0]} to ${this.formatCurrency(lastValue)} in ${labels[labels.length - 1]}.`; + } else { + summary += `Prices remained relatively stable throughout the period, ending at ${this.formatCurrency(lastValue)}.`; + } + + return summary; + } + createLineChart(data, parsedQuery) { if (!data || !Array.isArray(data) || data.length === 0) { return { type: 'line', title: 'No Data Available', + summary: 'No data available for this chart.', data: { labels: [], datasets: [] }, options: { responsive: true } }; @@ -176,7 +219,7 @@ class ChartFormatter { const labels = data.map(row => row.month || row.date || row.period || 'Unknown'); const values = data.map(row => Math.round(row.avg_price || row.avg_value || 0)); - return { + const chart = { type: 'line', title: 'Price Trend Over Time', data: { @@ -208,6 +251,53 @@ class ChartFormatter { } } }; + + // Add summary + chart.summary = this.generateLineChartSummary(chart, data, parsedQuery); + + return chart; + } + + generateMultiAreaLineChartSummary(chart, data, parsedQuery) { + const { labels, datasets } = chart.data; + const areaCount = datasets.length; + + if (datasets.length === 0) { + return "No data available for comparison."; + } + + // Find highest and lowest areas by calculating averages + const areaStats = datasets.map(ds => { + const validData = ds.data.filter(v => v !== null && v !== undefined && !isNaN(v)); + if (validData.length === 0) return null; + + const avg = validData.reduce((a, b) => a + b, 0) / validData.length; + const total = validData.reduce((a, b) => a + b, 0); + const max = Math.max(...validData); + const min = Math.min(...validData); + + return { + area: ds.label, + avg: avg, + total: total, + max: max, + min: min + }; + }).filter(stat => stat !== null); + + if (areaStats.length === 0) { + return "No valid data available for area comparison."; + } + + const highest = areaStats.reduce((a, b) => a.avg > b.avg ? a : b); + const lowest = areaStats.reduce((a, b) => a.avg < b.avg ? a : b); + + let summary = `This multi-area comparison chart displays price trends across ${areaCount} area${areaCount > 1 ? 's' : ''}. `; + summary += `${highest.area} shows the highest average price at ${this.formatCurrency(Math.round(highest.avg))}, `; + summary += `while ${lowest.area} has the lowest at ${this.formatCurrency(Math.round(lowest.avg))}. `; + summary += `The chart spans ${labels.length} time period${labels.length > 1 ? 's' : ''}, showing how rental prices have evolved across different areas.`; + + return summary; } createMultiAreaLineChart(data, parsedQuery) { @@ -262,7 +352,7 @@ class ChartFormatter { }); }); - return { + const chart = { type: 'line', title: 'Average Price by Area Over Time', data: { @@ -297,6 +387,48 @@ class ChartFormatter { } } }; + + // Add summary + chart.summary = this.generateMultiAreaLineChartSummary(chart, data, parsedQuery); + + return chart; + } + + generateBarChartSummary(chart, data, parsedQuery) { + const { labels, datasets } = chart.data; + const values = datasets[0]?.data || []; + + if (values.length === 0) { + return "No data available for comparison."; + } + + const validValues = values.filter(v => v !== null && v !== undefined && !isNaN(v) && v > 0); + if (validValues.length === 0) { + return "No valid data points available for this chart."; + } + + const maxValue = Math.max(...validValues); + const maxIndex = values.indexOf(maxValue); + const topItem = labels[maxIndex]; + const avgValue = Math.round(validValues.reduce((a, b) => a + b, 0) / validValues.length); + + const isPriceData = parsedQuery.intent === 'compare'; + const metric = isPriceData ? 'average price' : 'transaction count'; + const unit = isPriceData ? this.formatCurrency(maxValue) : maxValue.toLocaleString(); + + let summary = `The bar chart compares ${metric} across ${labels.length} ${labels.length > 1 ? 'items' : 'item'}. `; + summary += `${topItem} leads with ${unit}, `; + summary += `significantly above the average of ${isPriceData ? this.formatCurrency(avgValue) : avgValue.toLocaleString()}. `; + + if (parsedQuery.original_query && parsedQuery.original_query.toLowerCase().includes('fast moving')) { + summary += `These projects show high transaction volume, indicating strong market activity and investor interest.`; + } else if (parsedQuery.original_query && parsedQuery.original_query.toLowerCase().includes('off-plan')) { + summary += `These areas demonstrate strong off-plan project activity, reflecting growing investor confidence and market demand.`; + } else { + summary += `This visualization helps identify the top-performing areas in the market.`; + } + + return summary; } createBarChart(data, parsedQuery) { @@ -304,6 +436,7 @@ class ChartFormatter { return { type: 'bar', title: 'No Data Available', + summary: 'No data available for this chart.', data: { labels: [], datasets: [] }, options: { responsive: true } }; @@ -311,7 +444,7 @@ class ChartFormatter { const labels = data.map(row => { // For fast moving projects, prioritize project names - if (parsedQuery.original_query.includes('fast moving') && row.project_en) { + if (parsedQuery.original_query && parsedQuery.original_query.includes('fast moving') && row.project_en) { return row.project_en; } @@ -338,9 +471,9 @@ class ChartFormatter { 0) ); - return { + const chart = { type: 'bar', - title: parsedQuery.original_query.includes('fast moving') ? 'Fast Moving Projects' : 'Area Comparison', + title: parsedQuery.original_query && parsedQuery.original_query.includes('fast moving') ? 'Fast Moving Projects' : 'Area Comparison', data: { labels: labels, datasets: [{ @@ -372,6 +505,44 @@ class ChartFormatter { } } }; + + // Add summary + chart.summary = this.generateBarChartSummary(chart, data, parsedQuery); + + return chart; + } + + generatePieChartSummary(chart, data, parsedQuery) { + const { labels, datasets } = chart.data; + const values = datasets[0]?.data || []; + const total = values.reduce((a, b) => a + b, 0); + + if (values.length === 0 || total === 0) { + return "No data available for distribution analysis."; + } + + // Find largest segment + const maxIndex = values.indexOf(Math.max(...values)); + const largestCategory = labels[maxIndex]; + const largestValue = values[maxIndex]; + const largestPercent = Math.round((largestValue / total) * 100); + + // Find smallest segment + const minIndex = values.indexOf(Math.min(...values.filter(v => v > 0))); + const smallestCategory = labels[minIndex]; + const smallestValue = values[minIndex]; + const smallestPercent = Math.round((smallestValue / total) * 100); + + let summary = `The pie chart shows the distribution across ${labels.length} categor${labels.length > 1 ? 'ies' : 'y'}. `; + summary += `${largestCategory} represents the largest share at ${largestPercent}% (${largestValue.toLocaleString()} ${largestValue === 1 ? 'transaction' : 'transactions'}). `; + + if (labels.length > 1 && smallestCategory !== largestCategory) { + summary += `${smallestCategory} has the smallest share at ${smallestPercent}%. `; + } + + summary += `The distribution provides insights into market composition and category preferences.`; + + return summary; } createPieChart(data, parsedQuery) { @@ -379,6 +550,7 @@ class ChartFormatter { return { type: 'pie', title: 'No Data Available', + summary: 'No data available for this chart.', data: { labels: [], datasets: [] }, options: { responsive: true } }; @@ -423,7 +595,7 @@ class ChartFormatter { return null; } - return { + const chart = { type: 'pie', title: 'Distribution Analysis', data: { @@ -444,6 +616,11 @@ class ChartFormatter { } } }; + + // Add summary + chart.summary = this.generatePieChartSummary(chart, data, parsedQuery); + + return chart; } createCards(data, parsedQuery) {