File: D:/web/hyperflight/apps/sales-analytics/index-v12.asp
<%
Option Explicit
Response.Buffer = True
Response.CodePage = 65001
Response.CharSet = "utf-8"
' ======================================================================
' HyperFlight Sales Analytics v19
' Standalone Bootstrap 5 / Classic ASP / MySQL application
' Based on the original inc-template-sales-map.asp report
' ======================================================================
Const adCmdText = 1
Const adParamInput = 1
Const adVarChar = 200
Dim conn, cmd, rs
Dim trendConn, trendCmd, trendRs
Dim countryConn, countryCmd, countryRs
Dim startDate, endDate, requestedStartDate, requestedEndDate, dateRange, dateRangeName
Dim measure, chartView, ukMode, topLimit, previousYears, previousYearsMax, trendCountry, trendInterval
Dim validationMessage, dataError, rangeLabel
Dim sql, trendSql, countrySql, orderBySql, countryCodeExpr, countryNameExpr, effectiveCountryExpr
Dim dataRows, rowCount, i, tableCount, barCount, pieCount
Dim trendRows, trendRowCount, trendYear, trendMonth
Dim countryRows, countryRowCount, countryListStartDate, countryListEndDate
Dim countryListCode, countryListName, trendCountryFound, trendCountryName
Dim trendComparisonMode, trendSelectedYear, trendCurrentYear, trendCurrentMonth, trendEarliestYear, yearlyTrendAvailable
Dim trendQueryStartDate, trendQueryEndDate, trendSeriesYear, trendSeriesMonth, trendSeriesValue
Dim trendYearIndex, trendHistoricalYear
Dim trendHistoricalValues(10, 12), trendCurrentValues(12)
Dim countryCode, countryName, currentOrders, currentSales, currentValue
Dim totalOrders, totalSales, totalSelected, countryCount
Dim topCountryName, topCountryValue, topCountryShare, averageOrderValue
Dim pieOtherValue, pieDisplayedTotal, shareValue
Dim chartTitle, chartSubtitle, measureLabel, valueColumnLabel
Function IsoDate(ByVal value)
IsoDate = Year(value) & "-" & Right("0" & Month(value), 2) & "-" & Right("0" & Day(value), 2)
End Function
Function IsValidIsoDate(ByVal value)
Dim parts, testDate
IsValidIsoDate = False
If Len(value) <> 10 Then Exit Function
parts = Split(value, "-")
If UBound(parts) <> 2 Then Exit Function
If Not IsNumeric(parts(0)) Then Exit Function
If Not IsNumeric(parts(1)) Then Exit Function
If Not IsNumeric(parts(2)) Then Exit Function
On Error Resume Next
testDate = DateSerial(CInt(parts(0)), CInt(parts(1)), CInt(parts(2)))
If Err.Number = 0 Then
If Year(testDate) = CInt(parts(0)) And Month(testDate) = CInt(parts(1)) And Day(testDate) = CInt(parts(2)) Then
IsValidIsoDate = True
End If
End If
Err.Clear
On Error GoTo 0
End Function
Function ParseIsoDate(ByVal value)
Dim parts
parts = Split(value, "-")
ParseIsoDate = DateSerial(CInt(parts(0)), CInt(parts(1)), CInt(parts(2)))
End Function
Function FriendlyDate(ByVal value)
FriendlyDate = Day(value) & " " & MonthName(Month(value), True) & " " & Year(value)
End Function
Function Html(ByVal value)
If IsNull(value) Then
Html = ""
Else
Html = Server.HTMLEncode(CStr(value))
End If
End Function
Function JsString(ByVal value)
Dim textValue
If IsNull(value) Then
JsString = ""
Exit Function
End If
textValue = CStr(value)
textValue = Replace(textValue, "\", "\\")
textValue = Replace(textValue, Chr(34), "\" & Chr(34))
textValue = Replace(textValue, "'", "\'")
textValue = Replace(textValue, vbCr, "\r")
textValue = Replace(textValue, vbLf, "\n")
textValue = Replace(textValue, "</", "<\/")
JsString = textValue
End Function
Function JsNumber(ByVal value)
Dim textValue
If IsNull(value) Or IsEmpty(value) Then
JsNumber = "0"
Else
textValue = CStr(CDbl(value))
textValue = Replace(textValue, ",", ".")
JsNumber = textValue
End If
End Function
Function Number0(ByVal value)
If IsNull(value) Or IsEmpty(value) Then
Number0 = "0"
Else
Number0 = FormatNumber(CDbl(value), 0)
End If
End Function
Function Money0(ByVal value)
If IsNull(value) Or IsEmpty(value) Then
Money0 = "£0"
Else
Money0 = "£" & FormatNumber(CDbl(value), 0)
End If
End Function
Function BuildViewUrl(ByVal requestedView)
BuildViewUrl = "?DateRange=" & Server.URLEncode(dateRange) & _
"&StartDate=" & Server.URLEncode(startDate) & _
"&EndDate=" & Server.URLEncode(endDate) & _
"&Measure=" & Server.URLEncode(measure) & _
"&UK=" & Server.URLEncode(ukMode) & _
"&TopLimit=" & topLimit & _
"&PreviousYears=" & previousYears & _
"&TrendCountry=" & Server.URLEncode(trendCountry) & _
"&TrendInterval=" & Server.URLEncode(trendInterval) & _
"&View=" & Server.URLEncode(requestedView)
End Function
Function BuildTrendIntervalUrl(ByVal requestedInterval)
BuildTrendIntervalUrl = "?DateRange=" & Server.URLEncode(dateRange) & _
"&StartDate=" & Server.URLEncode(startDate) & _
"&EndDate=" & Server.URLEncode(endDate) & _
"&Measure=" & Server.URLEncode(measure) & _
"&UK=" & Server.URLEncode(ukMode) & _
"&TopLimit=" & topLimit & _
"&PreviousYears=" & previousYears & _
"&TrendCountry=" & Server.URLEncode(trendCountry) & _
"&TrendInterval=" & Server.URLEncode(requestedInterval) & _
"&View=trend"
End Function
Function BuildProductAnalysisUrl()
BuildProductAnalysisUrl = "products.asp?DateRange=" & Server.URLEncode(dateRange) & _
"&StartDate=" & Server.URLEncode(startDate) & _
"&EndDate=" & Server.URLEncode(endDate) & _
"&Measure=sales" & _
"&UK=" & Server.URLEncode(ukMode) & _
"&Country=" & Server.URLEncode(trendCountry) & _
"&Breakdown=category&View=bar"
End Function
' ----------------------------------------------------------------------
' Filters and defaults
' ----------------------------------------------------------------------
requestedStartDate = Trim(Request.QueryString("StartDate"))
requestedEndDate = Trim(Request.QueryString("EndDate"))
dateRange = LCase(Trim(Request.QueryString("DateRange")))
measure = LCase(Trim(Request.QueryString("Measure")))
chartView = LCase(Trim(Request.QueryString("View")))
ukMode = LCase(Trim(Request.QueryString("UK")))
topLimit = Trim(Request.QueryString("TopLimit"))
previousYears = Trim(Request.QueryString("PreviousYears"))
trendCountry = Trim(Request.QueryString("TrendCountry"))
trendInterval = LCase(Trim(Request.QueryString("TrendInterval")))
' Existing bookmarked URLs containing dates but no DateRange remain valid.
If dateRange = "" Then
If requestedStartDate <> "" Or requestedEndDate <> "" Then
dateRange = "custom"
Else
dateRange = "12months"
End If
End If
endDate = IsoDate(Date())
Select Case dateRange
Case "30days"
dateRangeName = "Past 30 days"
startDate = IsoDate(DateAdd("d", -29, Date()))
Case "60days"
dateRangeName = "Past 60 days"
startDate = IsoDate(DateAdd("d", -59, Date()))
Case "90days"
dateRangeName = "Past 90 days"
startDate = IsoDate(DateAdd("d", -89, Date()))
Case "thisquarter"
dateRangeName = "This quarter"
startDate = IsoDate(DateSerial(Year(Date()), ((Month(Date()) - 1) \ 3) * 3 + 1, 1))
Case "previousquarter"
dateRangeName = "Previous quarter"
startDate = IsoDate(DateAdd("m", -3, DateSerial(Year(Date()), ((Month(Date()) - 1) \ 3) * 3 + 1, 1)))
endDate = IsoDate(DateAdd("d", -1, DateSerial(Year(Date()), ((Month(Date()) - 1) \ 3) * 3 + 1, 1)))
Case "6months"
dateRangeName = "Past 6 months"
startDate = IsoDate(DateAdd("m", -6, Date()))
Case "12months"
dateRangeName = "Past 12 months"
startDate = IsoDate(DateAdd("m", -12, Date()))
Case "yeartodate"
dateRangeName = "Year to date"
startDate = IsoDate(DateSerial(Year(Date()), 1, 1))
Case "lastyear"
dateRangeName = "Last calendar year"
startDate = IsoDate(DateSerial(Year(Date()) - 1, 1, 1))
endDate = IsoDate(DateSerial(Year(Date()) - 1, 12, 31))
Case "2years"
dateRangeName = "Past 2 years"
startDate = IsoDate(DateAdd("yyyy", -2, Date()))
Case "3years"
dateRangeName = "Past 3 years"
startDate = IsoDate(DateAdd("yyyy", -3, Date()))
Case "5years"
dateRangeName = "Past 5 years"
startDate = IsoDate(DateAdd("yyyy", -5, Date()))
Case "all"
dateRangeName = "All time"
startDate = ""
Case "custom"
dateRangeName = "Custom dates"
startDate = requestedStartDate
endDate = requestedEndDate
Case Else
dateRange = "12months"
dateRangeName = "Past 12 months"
startDate = IsoDate(DateAdd("m", -12, Date()))
End Select
If measure = "" Then measure = "sales"
If chartView = "" Then chartView = "bar"
If ukMode = "" Then ukMode = "include"
If topLimit = "" Or Not IsNumeric(topLimit) Then topLimit = 15
topLimit = CInt(topLimit)
If topLimit <> 10 And topLimit <> 15 And topLimit <> 20 And topLimit <> 30 Then topLimit = 15
If previousYears = "" Or Not IsNumeric(previousYears) Then previousYears = 1
previousYears = CInt(previousYears)
If previousYears < 1 Then previousYears = 1
If previousYears > 25 Then previousYears = 25
If trendCountry = "" Or LCase(trendCountry) = "all" Then
trendCountry = "all"
Else
trendCountry = UCase(trendCountry)
End If
Select Case trendInterval
Case "monthly", "yearly"
' Valid.
Case Else
trendInterval = "monthly"
End Select
Select Case measure
Case "sales", "orders"
' Valid.
Case Else
measure = "sales"
End Select
Select Case chartView
Case "world", "europe", "bar", "pie", "trend"
' Valid.
Case Else
chartView = "bar"
End Select
Select Case ukMode
Case "include", "exclude"
' Valid.
Case Else
ukMode = "include"
End Select
validationMessage = ""
If dateRange = "all" Then
If Not IsValidIsoDate(endDate) Then validationMessage = "Please enter a valid end date."
ElseIf Not IsValidIsoDate(startDate) Or Not IsValidIsoDate(endDate) Then
validationMessage = "Please enter valid start and end dates."
ElseIf ParseIsoDate(startDate) > ParseIsoDate(endDate) Then
validationMessage = "The start date must not be later than the end date."
End If
yearlyTrendAvailable = False
If validationMessage = "" Then
If dateRange = "all" Or dateRange = "lastyear" Then
yearlyTrendAvailable = True
ElseIf IsValidIsoDate(startDate) And IsValidIsoDate(endDate) Then
If DateDiff("d", ParseIsoDate(startDate), ParseIsoDate(endDate)) >= 365 Then yearlyTrendAvailable = True
End If
End If
If Not yearlyTrendAvailable And trendInterval = "yearly" Then trendInterval = "monthly"
If trendInterval = "yearly" Then
previousYearsMax = 25
Else
previousYearsMax = 10
End If
If previousYears > previousYearsMax Then previousYears = previousYearsMax
If validationMessage = "" Then
If dateRange = "all" Then
rangeLabel = Html(dateRangeName)
Else
rangeLabel = Html(dateRangeName) & " · " & _
Html(FriendlyDate(ParseIsoDate(startDate))) & " to " & _
Html(FriendlyDate(ParseIsoDate(endDate)))
End If
Else
rangeLabel = Html(dateRangeName)
End If
If measure = "orders" Then
orderBySql = "OrderCount"
measureLabel = "Orders"
valueColumnLabel = "Orders"
Else
orderBySql = "SalesTotal"
measureLabel = "Sales value"
valueColumnLabel = "Sales"
End If
Select Case chartView
Case "europe"
chartTitle = "Europe map"
Case "bar"
chartTitle = "Top countries"
Case "pie"
chartTitle = "Country share"
Case "trend"
If trendInterval = "yearly" Then
If measure = "orders" Then
chartTitle = "Yearly order trend"
Else
chartTitle = "Yearly sales trend"
End If
Else
If measure = "orders" Then
chartTitle = "Monthly order trend"
Else
chartTitle = "Monthly sales trend"
End If
End If
Case Else
chartTitle = "World map"
End Select
trendComparisonMode = False
If chartView = "trend" And dateRange = "lastyear" Then
trendComparisonMode = True
trendCurrentYear = Year(Date())
trendSelectedYear = trendCurrentYear - 1
trendEarliestYear = trendSelectedYear - previousYears
trendCurrentMonth = Month(Date())
End If
If chartView = "trend" Then
If trendInterval = "yearly" Then
If trendComparisonMode Then
chartSubtitle = measureLabel & " by year: " & trendEarliestYear & " to " & trendCurrentYear & " year to date"
Else
chartSubtitle = measureLabel & " by year"
End If
ElseIf trendComparisonMode Then
If previousYears = 1 Then
chartSubtitle = measureLabel & " by month: " & trendEarliestYear & ", " & trendSelectedYear & " and " & trendCurrentYear & " year to date"
Else
chartSubtitle = measureLabel & " by month: " & trendEarliestYear & " to " & trendSelectedYear & " and " & trendCurrentYear & " year to date"
End If
Else
chartSubtitle = measureLabel & " by month"
End If
Else
chartSubtitle = measureLabel & " by country"
End If
' ----------------------------------------------------------------------
' Query and aggregate data
' ----------------------------------------------------------------------
rowCount = 0
totalOrders = 0
totalSales = 0
totalSelected = 0
countryCount = 0
topCountryName = ""
topCountryValue = 0
topCountryShare = 0
averageOrderValue = 0
pieOtherValue = 0
trendRowCount = 0
countryRowCount = 0
trendCountryFound = False
trendCountryName = "All countries"
dataError = ""
If validationMessage = "" Then
effectiveCountryExpr = "COALESCE(NULLIF(TRIM(o.DeliveryCountry), ''), TRIM(o.Country))"
countryCodeExpr = "CASE WHEN LEFT(c.Country, 14) = 'United Kingdom' THEN 'GB' ELSE UPPER(c.CodeA2) END"
countryNameExpr = "CASE WHEN LEFT(c.Country, 14) = 'United Kingdom' THEN 'United Kingdom' ELSE c.Country END"
sql = "SELECT " & countryCodeExpr & " AS CountryCode, " & _
countryNameExpr & " AS CountryName, " & _
"COUNT(*) AS OrderCount, " & _
"SUM(IFNULL(o.GrandTotal, 0) - IFNULL(o.VATIncluded, 0)) AS SalesTotal " & _
"FROM orders o " & _
"INNER JOIN countries c ON c.Country = " & effectiveCountryExpr & " " & _
"WHERE o.IsInvoice = TRUE " & _
"AND o.PaymentReceived = TRUE " & _
"AND o.Status <> 'CANCELLED' " & _
"AND o.Status <> 'ORDER PLACED' "
If dateRange <> "all" Then
sql = sql & "AND o.DateTimePaid >= ? "
End If
sql = sql & "AND o.DateTimePaid < DATE_ADD(?, INTERVAL 1 DAY) " & _
"AND TRIM(IFNULL(c.CodeA2, '')) <> '' "
If ukMode = "exclude" Then
sql = sql & "AND LEFT(c.Country, 14) <> 'United Kingdom' "
End If
sql = sql & "GROUP BY " & countryCodeExpr & ", " & countryNameExpr & " " & _
"ORDER BY " & orderBySql & " DESC, CountryName ASC"
On Error Resume Next
Set conn = Server.CreateObject("ADODB.Connection")
conn.Open "DSN=MySQL_hyperflight;"
If Err.Number = 0 Then
Set cmd = Server.CreateObject("ADODB.Command")
Set cmd.ActiveConnection = conn
cmd.CommandType = adCmdText
cmd.CommandText = sql
If dateRange <> "all" Then
cmd.Parameters.Append cmd.CreateParameter("@DateFrom", adVarChar, adParamInput, 10, startDate)
End If
cmd.Parameters.Append cmd.CreateParameter("@DateTo", adVarChar, adParamInput, 10, endDate)
Set rs = cmd.Execute()
End If
If Err.Number <> 0 Then
dataError = Err.Description
Err.Clear
ElseIf Not rs.EOF Then
dataRows = rs.GetRows()
rowCount = UBound(dataRows, 2) + 1
End If
If IsObject(rs) Then
If rs.State <> 0 Then rs.Close
Set rs = Nothing
End If
Set cmd = Nothing
If IsObject(conn) Then
If conn.State <> 0 Then conn.Close
Set conn = Nothing
End If
On Error GoTo 0
If dataError = "" And rowCount > 0 Then
For i = 0 To rowCount - 1
currentOrders = 0
currentSales = 0
If Not IsNull(dataRows(2, i)) Then currentOrders = CLng(dataRows(2, i))
If Not IsNull(dataRows(3, i)) Then currentSales = CDbl(dataRows(3, i))
totalOrders = totalOrders + currentOrders
totalSales = totalSales + currentSales
Next
If measure = "orders" Then
totalSelected = CDbl(totalOrders)
Else
totalSelected = totalSales
End If
countryCount = rowCount
topCountryName = CStr(dataRows(1, 0))
If measure = "orders" Then
topCountryValue = CDbl(dataRows(2, 0))
Else
topCountryValue = CDbl(dataRows(3, 0))
End If
If totalSelected > 0 Then topCountryShare = (topCountryValue / totalSelected) * 100
If totalOrders > 0 Then averageOrderValue = totalSales / totalOrders
End If
End If
' Load countries available to the Sales trend filter.
If validationMessage = "" And dataError = "" And rowCount > 0 And chartView = "trend" Then
countryListStartDate = startDate
countryListEndDate = endDate
If trendComparisonMode Then
countryListStartDate = IsoDate(DateSerial(trendEarliestYear, 1, 1))
countryListEndDate = IsoDate(Date())
End If
countrySql = "SELECT " & countryCodeExpr & " AS CountryCode, " & _
countryNameExpr & " AS CountryName " & _
"FROM orders o " & _
"INNER JOIN countries c ON c.Country = " & effectiveCountryExpr & " " & _
"WHERE o.IsInvoice = TRUE " & _
"AND o.PaymentReceived = TRUE " & _
"AND o.Status <> 'CANCELLED' " & _
"AND o.Status <> 'ORDER PLACED' "
If countryListStartDate <> "" Then
countrySql = countrySql & "AND o.DateTimePaid >= ? "
End If
countrySql = countrySql & "AND o.DateTimePaid < DATE_ADD(?, INTERVAL 1 DAY) " & _
"AND TRIM(IFNULL(c.CodeA2, '')) <> '' "
If ukMode = "exclude" Then
countrySql = countrySql & "AND LEFT(c.Country, 14) <> 'United Kingdom' "
End If
countrySql = countrySql & "GROUP BY " & countryCodeExpr & ", " & countryNameExpr & " " & _
"ORDER BY CountryName ASC"
On Error Resume Next
Set countryConn = Server.CreateObject("ADODB.Connection")
countryConn.Open "DSN=MySQL_hyperflight;"
If Err.Number = 0 Then
Set countryCmd = Server.CreateObject("ADODB.Command")
Set countryCmd.ActiveConnection = countryConn
countryCmd.CommandType = adCmdText
countryCmd.CommandText = countrySql
If countryListStartDate <> "" Then
countryCmd.Parameters.Append countryCmd.CreateParameter("@CountryDateFrom", adVarChar, adParamInput, 10, countryListStartDate)
End If
countryCmd.Parameters.Append countryCmd.CreateParameter("@CountryDateTo", adVarChar, adParamInput, 10, countryListEndDate)
Set countryRs = countryCmd.Execute()
End If
If Err.Number <> 0 Then
dataError = Err.Description
Err.Clear
ElseIf Not countryRs.EOF Then
countryRows = countryRs.GetRows()
countryRowCount = UBound(countryRows, 2) + 1
End If
If IsObject(countryRs) Then
If countryRs.State <> 0 Then countryRs.Close
Set countryRs = Nothing
End If
Set countryCmd = Nothing
If IsObject(countryConn) Then
If countryConn.State <> 0 Then countryConn.Close
Set countryConn = Nothing
End If
On Error GoTo 0
If dataError = "" Then
If trendCountry = "all" Then
trendCountryFound = True
Else
For i = 0 To countryRowCount - 1
countryListCode = UCase(CStr(countryRows(0, i)))
If countryListCode = trendCountry Then
trendCountryFound = True
trendCountryName = CStr(countryRows(1, i))
Exit For
End If
Next
End If
If Not trendCountryFound Then
trendCountry = "all"
trendCountryName = "All countries"
End If
If trendCountry <> "all" Then
chartSubtitle = chartSubtitle & " (" & trendCountryName & ")"
End If
End If
End If
' Load monthly totals only when the trend view is requested.
If validationMessage = "" And dataError = "" And rowCount > 0 And chartView = "trend" Then
trendQueryStartDate = startDate
trendQueryEndDate = endDate
If trendComparisonMode Then
trendQueryStartDate = IsoDate(DateSerial(trendEarliestYear, 1, 1))
trendQueryEndDate = IsoDate(Date())
End If
trendSql = "SELECT YEAR(o.DateTimePaid) AS SalesYear, " & _
"MONTH(o.DateTimePaid) AS SalesMonth, " & _
"COUNT(*) AS OrderCount, " & _
"SUM(IFNULL(o.GrandTotal, 0) - IFNULL(o.VATIncluded, 0)) AS SalesTotal " & _
"FROM orders o " & _
"INNER JOIN countries c ON c.Country = " & effectiveCountryExpr & " " & _
"WHERE o.IsInvoice = TRUE " & _
"AND o.PaymentReceived = TRUE " & _
"AND o.Status <> 'CANCELLED' " & _
"AND o.Status <> 'ORDER PLACED' "
If dateRange <> "all" Then
trendSql = trendSql & "AND o.DateTimePaid >= ? "
End If
trendSql = trendSql & "AND o.DateTimePaid < DATE_ADD(?, INTERVAL 1 DAY) " & _
"AND TRIM(IFNULL(c.CodeA2, '')) <> '' "
If ukMode = "exclude" Then
trendSql = trendSql & "AND LEFT(c.Country, 14) <> 'United Kingdom' "
End If
If trendCountry <> "all" Then
trendSql = trendSql & "AND " & countryCodeExpr & " = ? "
End If
trendSql = trendSql & "GROUP BY YEAR(o.DateTimePaid), MONTH(o.DateTimePaid) " & _
"ORDER BY SalesYear ASC, SalesMonth ASC"
On Error Resume Next
Set trendConn = Server.CreateObject("ADODB.Connection")
trendConn.Open "DSN=MySQL_hyperflight;"
If Err.Number = 0 Then
Set trendCmd = Server.CreateObject("ADODB.Command")
Set trendCmd.ActiveConnection = trendConn
trendCmd.CommandType = adCmdText
trendCmd.CommandText = trendSql
If dateRange <> "all" Then
trendCmd.Parameters.Append trendCmd.CreateParameter("@TrendDateFrom", adVarChar, adParamInput, 10, trendQueryStartDate)
End If
trendCmd.Parameters.Append trendCmd.CreateParameter("@TrendDateTo", adVarChar, adParamInput, 10, trendQueryEndDate)
If trendCountry <> "all" Then
trendCmd.Parameters.Append trendCmd.CreateParameter("@TrendCountry", adVarChar, adParamInput, 10, trendCountry)
End If
Set trendRs = trendCmd.Execute()
End If
If Err.Number <> 0 Then
dataError = Err.Description
Err.Clear
ElseIf Not trendRs.EOF Then
trendRows = trendRs.GetRows()
trendRowCount = UBound(trendRows, 2) + 1
End If
If IsObject(trendRs) Then
If trendRs.State <> 0 Then trendRs.Close
Set trendRs = Nothing
End If
Set trendCmd = Nothing
If IsObject(trendConn) Then
If trendConn.State <> 0 Then trendConn.Close
Set trendConn = Nothing
End If
On Error GoTo 0
If trendComparisonMode And trendInterval = "monthly" And trendRowCount > 0 Then
For i = 0 To trendRowCount - 1
trendSeriesYear = CInt(trendRows(0, i))
trendSeriesMonth = CInt(trendRows(1, i))
If measure = "orders" Then
trendSeriesValue = CDbl(trendRows(2, i))
Else
trendSeriesValue = CDbl(trendRows(3, i))
End If
If trendSeriesYear >= trendEarliestYear And trendSeriesYear <= trendSelectedYear Then
trendYearIndex = trendSeriesYear - trendEarliestYear
trendHistoricalValues(trendYearIndex, trendSeriesMonth) = trendSeriesValue
ElseIf trendSeriesYear = trendCurrentYear Then
trendCurrentValues(trendSeriesMonth) = trendSeriesValue
End If
Next
End If
End If
tableCount = rowCount
If tableCount > topLimit Then tableCount = topLimit
barCount = tableCount
pieCount = rowCount
If pieCount > 10 Then pieCount = 10
If pieCount > topLimit Then pieCount = topLimit
pieDisplayedTotal = 0
If rowCount > 0 Then
For i = 0 To pieCount - 1
If measure = "orders" Then
pieDisplayedTotal = pieDisplayedTotal + CDbl(dataRows(2, i))
Else
pieDisplayedTotal = pieDisplayedTotal + CDbl(dataRows(3, i))
End If
Next
pieOtherValue = totalSelected - pieDisplayedTotal
If pieOtherValue < 0.000001 Then pieOtherValue = 0
End If
%>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>HyperFlight Sales Analytics</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
<% If validationMessage = "" And dataError = "" And rowCount > 0 Then %>
<script src="https://www.gstatic.com/charts/loader.js"></script>
<% End If %>
<style>
body { background: #f5f6f8; }
.report-wrap { max-width: 1500px; }
.metric-card { min-height: 118px; }
.metric-value { font-size: 1.75rem; font-weight: 700; line-height: 1.15; }
.metric-value.metric-text { font-size: 1.25rem; }
.metric-label { color: #6c757d; font-size: 0.9rem; }
.card-header h2 { font-size: 1.05rem; margin: 0; }
.chart-shell { position: relative; min-height: 500px; }
#sales-chart { width: 100%; min-height: 500px; }
.chart-loading {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.88);
}
.chart-header-title { min-width: 0; flex: 1 1 auto; }
.chart-header-actions { flex: 0 0 auto; }
.view-nav { flex-wrap: nowrap !important; }
.view-nav .btn { min-width: 100px; white-space: nowrap; }
.trend-filters-form { flex-wrap: nowrap !important; }
.trend-filters-form .previous-years-select { width: 72px; min-width: 72px; }
.trend-filters-form .country-select { width: 180px; min-width: 180px; max-width: 180px; }
.country-name { min-width: 190px; }
.table th { white-space: nowrap; }
.small-note { font-size: 0.875rem; color: #6c757d; }
.trend-interval-bar { min-height: 38px; }
.trend-interval-bar .btn { min-width: 72px; }
.analysis-nav .btn { min-width: 145px; }
@media (max-width: 1399.98px) {
.chart-header-actions { flex-wrap: wrap; }
.trend-filters-form { flex-wrap: wrap !important; }
.view-nav { flex-wrap: wrap !important; }
}
@media (max-width: 767.98px) {
.chart-shell, #sales-chart { min-height: 420px; }
.view-nav .btn { min-width: 0; }
.trend-filters-form .country-select { width: 100%; min-width: 180px; max-width: none; }
}
</style>
</head>
<body class="pb-5">
<div class="container-fluid report-wrap py-4 px-3 px-lg-4">
<div class="card shadow-sm mb-4">
<div class="card-header bg-primary text-white py-3">
<div class="d-flex flex-column flex-lg-row justify-content-between gap-2 align-items-lg-center">
<div>
<h1 class="h4 mb-1">HyperFlight Sales Analytics</h1>
<div class="small opacity-75">Interactive geographical and product sales reporting</div>
</div>
<div class="d-flex flex-column flex-sm-row align-items-sm-center gap-2">
<div class="btn-group btn-group-sm analysis-nav" role="group" aria-label="Analysis section">
<span class="btn btn-light active" aria-current="page">Geographical analysis</span>
<a class="btn btn-light" href="<%=Html(BuildProductAnalysisUrl())%>">Product analysis</a>
</div>
<span class="badge text-bg-light"><%=rangeLabel%></span>
</div>
</div>
</div>
<div class="card-body">
<form method="get" action="" class="row g-3 align-items-end">
<div class="col-sm-6 col-lg-2">
<label for="DateRange" class="form-label fw-semibold">Date range</label>
<select class="form-select" id="DateRange" name="DateRange">
<option value="30days" <% If dateRange = "30days" Then Response.Write "selected" %>>Past 30 days</option>
<option value="60days" <% If dateRange = "60days" Then Response.Write "selected" %>>Past 60 days</option>
<option value="90days" <% If dateRange = "90days" Then Response.Write "selected" %>>Past 90 days</option>
<option value="thisquarter" <% If dateRange = "thisquarter" Then Response.Write "selected" %>>This quarter</option>
<option value="previousquarter" <% If dateRange = "previousquarter" Then Response.Write "selected" %>>Previous quarter</option>
<option value="6months" <% If dateRange = "6months" Then Response.Write "selected" %>>Past 6 months</option>
<option value="12months" <% If dateRange = "12months" Then Response.Write "selected" %>>Past 12 months</option>
<option value="yeartodate" <% If dateRange = "yeartodate" Then Response.Write "selected" %>>Year to date</option>
<option value="lastyear" <% If dateRange = "lastyear" Then Response.Write "selected" %>>Last calendar year</option>
<option value="2years" <% If dateRange = "2years" Then Response.Write "selected" %>>Past 2 years</option>
<option value="3years" <% If dateRange = "3years" Then Response.Write "selected" %>>Past 3 years</option>
<option value="5years" <% If dateRange = "5years" Then Response.Write "selected" %>>Past 5 years</option>
<option value="all" <% If dateRange = "all" Then Response.Write "selected" %>>All time</option>
<option value="custom" <% If dateRange = "custom" Then Response.Write "selected" %>>Custom dates</option>
</select>
</div>
<div class="col-sm-6 col-lg-2">
<label for="StartDate" class="form-label fw-semibold">Start date</label>
<input type="date" class="form-control" id="StartDate" name="StartDate" value="<%=Html(startDate)%>"<% If dateRange = "custom" Then Response.Write " required" Else Response.Write " readonly" %>>
</div>
<div class="col-sm-6 col-lg-2">
<label for="EndDate" class="form-label fw-semibold">End date</label>
<input type="date" class="form-control" id="EndDate" name="EndDate" value="<%=Html(endDate)%>"<% If dateRange = "custom" Then Response.Write " required" Else Response.Write " readonly" %>>
</div>
<div class="col-sm-6 col-lg-2">
<label for="Measure" class="form-label fw-semibold">Measure</label>
<select class="form-select" id="Measure" name="Measure">
<option value="sales" <% If measure = "sales" Then Response.Write "selected" %>>Sales value</option>
<option value="orders" <% If measure = "orders" Then Response.Write "selected" %>>Number of orders</option>
</select>
</div>
<div class="col-sm-6 col-lg-2">
<label for="UK" class="form-label fw-semibold">United Kingdom</label>
<select class="form-select" id="UK" name="UK">
<option value="include" <% If ukMode = "include" Then Response.Write "selected" %>>Include</option>
<option value="exclude" <% If ukMode = "exclude" Then Response.Write "selected" %>>Exclude</option>
</select>
</div>
<div class="col-sm-3 col-lg-1">
<label for="TopLimit" class="form-label fw-semibold">Top</label>
<select class="form-select" id="TopLimit" name="TopLimit">
<option value="10" <% If topLimit = 10 Then Response.Write "selected" %>>10</option>
<option value="15" <% If topLimit = 15 Then Response.Write "selected" %>>15</option>
<option value="20" <% If topLimit = 20 Then Response.Write "selected" %>>20</option>
<option value="30" <% If topLimit = 30 Then Response.Write "selected" %>>30</option>
</select>
</div>
<div class="col-sm-3 col-lg-1 d-grid">
<input type="hidden" name="PreviousYears" value="<%=previousYears%>">
<input type="hidden" name="TrendCountry" value="<%=Html(trendCountry)%>">
<input type="hidden" name="TrendInterval" value="<%=Html(trendInterval)%>">
<input type="hidden" name="View" value="<%=Html(chartView)%>">
<button type="submit" class="btn btn-primary fw-semibold">Run</button>
</div>
</form>
<div class="small-note mt-3">Sales values exclude VAT. Countries are based on the delivery address, falling back to the billing address where no delivery country is recorded. Partial refunds are not taken into account.</div>
</div>
</div>
<% If validationMessage <> "" Then %>
<div class="alert alert-warning shadow-sm"><%=Html(validationMessage)%></div>
<% ElseIf dataError <> "" Then %>
<div class="alert alert-danger shadow-sm">
<strong>The report could not be loaded.</strong>
<div class="small mt-1"><%=Html(dataError)%></div>
</div>
<% ElseIf rowCount = 0 Then %>
<div class="alert alert-info shadow-sm">No sales were found for the selected filters.</div>
<% Else %>
<div class="row g-3 mb-4">
<div class="col-6 col-lg-4 col-xxl-2">
<div class="card shadow-sm h-100 metric-card"><div class="card-body">
<div class="metric-value"><%=Money0(totalSales)%></div>
<div class="metric-label">Sales total</div>
</div></div>
</div>
<div class="col-6 col-lg-4 col-xxl-2">
<div class="card shadow-sm h-100 metric-card"><div class="card-body">
<div class="metric-value"><%=Number0(totalOrders)%></div>
<div class="metric-label">Orders</div>
</div></div>
</div>
<div class="col-6 col-lg-4 col-xxl-2">
<div class="card shadow-sm h-100 metric-card"><div class="card-body">
<div class="metric-value"><%=Number0(countryCount)%></div>
<div class="metric-label">Countries represented</div>
</div></div>
</div>
<div class="col-6 col-lg-4 col-xxl-2">
<div class="card shadow-sm h-100 metric-card border-primary"><div class="card-body">
<div class="metric-value metric-text"><%=Html(topCountryName)%></div>
<div class="metric-label">Largest market by <%=LCase(measureLabel)%></div>
</div></div>
</div>
<div class="col-6 col-lg-4 col-xxl-2">
<div class="card shadow-sm h-100 metric-card"><div class="card-body">
<div class="metric-value"><%=FormatNumber(topCountryShare, 1)%>%</div>
<div class="metric-label">Largest market share</div>
</div></div>
</div>
<div class="col-6 col-lg-4 col-xxl-2">
<div class="card shadow-sm h-100 metric-card"><div class="card-body">
<div class="metric-value"><%=Money0(averageOrderValue)%></div>
<div class="metric-label">Average order value</div>
</div></div>
</div>
</div>
<div class="card shadow-sm mb-4">
<div class="card-header bg-dark text-white py-3">
<div class="d-flex flex-column flex-xl-row justify-content-between align-items-xl-center gap-3">
<div class="chart-header-title">
<h2><%=Html(chartTitle)%></h2>
<div class="small opacity-75 mt-1"><%=Html(chartSubtitle)%></div>
</div>
<div class="d-flex flex-column flex-sm-row align-items-sm-center gap-2 chart-header-actions">
<% If chartView = "trend" Then %>
<form method="get" action="" class="d-flex align-items-center gap-2 trend-filters-form">
<input type="hidden" name="DateRange" value="<%=Html(dateRange)%>">
<input type="hidden" name="StartDate" value="<%=Html(startDate)%>">
<input type="hidden" name="EndDate" value="<%=Html(endDate)%>">
<input type="hidden" name="Measure" value="<%=Html(measure)%>">
<input type="hidden" name="UK" value="<%=Html(ukMode)%>">
<input type="hidden" name="TopLimit" value="<%=topLimit%>">
<input type="hidden" name="TrendInterval" value="<%=Html(trendInterval)%>">
<input type="hidden" name="View" value="trend">
<% If trendComparisonMode Then %>
<label for="PreviousYears" class="small fw-semibold text-nowrap mb-0">Previous years</label>
<select class="form-select form-select-sm previous-years-select" id="PreviousYears" name="PreviousYears" onchange="this.form.submit()" aria-label="Number of previous years to compare">
<% For i = 1 To previousYearsMax %>
<option value="<%=i%>"<% If previousYears = i Then Response.Write " selected" %>><%=i%></option>
<% Next %>
</select>
<% Else %>
<input type="hidden" name="PreviousYears" value="<%=previousYears%>">
<% End If %>
<label for="TrendCountry" class="small fw-semibold text-nowrap mb-0">Country</label>
<select class="form-select form-select-sm country-select" id="TrendCountry" name="TrendCountry" onchange="this.form.submit()" aria-label="Country for the sales trend">
<option value="all"<% If trendCountry = "all" Then Response.Write " selected" %>>All countries</option>
<% For i = 0 To countryRowCount - 1
countryListCode = CStr(countryRows(0, i))
countryListName = CStr(countryRows(1, i))
%>
<option value="<%=Html(countryListCode)%>"<% If trendCountry = UCase(countryListCode) Then Response.Write " selected" %>><%=Html(countryListName)%></option>
<% Next %>
</select>
</form>
<% End If %>
<div class="btn-group btn-group-sm view-nav" role="group" aria-label="Chart view">
<a class="btn btn-light<% If chartView = "bar" Then Response.Write " active" %>" href="<%=Html(BuildViewUrl("bar"))%>"<% If chartView = "bar" Then Response.Write " aria-current=""page""" %>>Bar chart</a>
<a class="btn btn-light<% If chartView = "pie" Then Response.Write " active" %>" href="<%=Html(BuildViewUrl("pie"))%>"<% If chartView = "pie" Then Response.Write " aria-current=""page""" %>>Pie chart</a>
<a class="btn btn-light<% If chartView = "trend" Then Response.Write " active" %>" href="<%=Html(BuildViewUrl("trend"))%>"<% If chartView = "trend" Then Response.Write " aria-current=""page""" %>>Sales trend</a>
<a class="btn btn-light<% If chartView = "world" Then Response.Write " active" %>" href="<%=Html(BuildViewUrl("world"))%>"<% If chartView = "world" Then Response.Write " aria-current=""page""" %>>World map</a>
<a class="btn btn-light<% If chartView = "europe" Then Response.Write " active" %>" href="<%=Html(BuildViewUrl("europe"))%>"<% If chartView = "europe" Then Response.Write " aria-current=""page""" %>>Europe map</a>
</div>
</div>
</div>
</div>
<div class="card-body p-2 p-md-3">
<% If chartView = "trend" Then %>
<div class="trend-interval-bar d-flex justify-content-end align-items-center gap-2 px-2 pb-2">
<span class="small fw-semibold text-muted">Trend interval</span>
<div class="btn-group btn-group-sm" role="group" aria-label="Trend interval">
<a class="btn btn-outline-secondary<% If trendInterval = "monthly" Then Response.Write " active" %>" href="<%=Html(BuildTrendIntervalUrl("monthly"))%>"<% If trendInterval = "monthly" Then Response.Write " aria-current=""page""" %>>Monthly</a>
<% If yearlyTrendAvailable Then %>
<a class="btn btn-outline-secondary<% If trendInterval = "yearly" Then Response.Write " active" %>" href="<%=Html(BuildTrendIntervalUrl("yearly"))%>"<% If trendInterval = "yearly" Then Response.Write " aria-current=""page""" %>>Yearly</a>
<% Else %>
<span class="btn btn-outline-secondary disabled" aria-disabled="true" title="Yearly trend is available for date ranges of at least 12 months">Yearly</span>
<% End If %>
</div>
</div>
<% End If %>
<div class="chart-shell">
<div id="chart-loading" class="chart-loading">
<div class="text-center">
<div class="spinner-border text-primary mb-2" role="status"><span class="visually-hidden">Loading chart</span></div>
<div class="small text-muted">Loading chart…</div>
</div>
</div>
<div id="sales-chart" aria-label="<%=Html(chartTitle)%>"></div>
</div>
<% If chartView = "pie" And rowCount > pieCount Then %>
<div class="small-note px-2 pb-1">The pie chart shows the top <%=pieCount%> countries; the remainder are grouped as Other.</div>
<% ElseIf chartView = "bar" And rowCount > barCount Then %>
<div class="small-note px-2 pb-1">The bar chart shows the top <%=barCount%> countries.</div>
<% ElseIf chartView = "trend" Then %>
<% If trendInterval = "yearly" Then %>
<% If trendComparisonMode Then %>
<div class="small-note px-2 pb-1">The yearly trend compares <%=trendEarliestYear%> to <%=trendSelectedYear%> with <%=trendCurrentYear%> year to date.</div>
<% Else %>
<div class="small-note px-2 pb-1">The yearly trend groups results by calendar year. The first and last years may be partial where the selected dates do not cover complete calendar years.</div>
<% End If %>
<% ElseIf trendComparisonMode Then %>
<% If previousYears = 1 Then %>
<div class="small-note px-2 pb-1">The trend chart compares <%=trendEarliestYear%>, <%=trendSelectedYear%> and <%=trendCurrentYear%> year to date. <%=Html(MonthName(trendCurrentMonth) & " " & trendCurrentYear)%> is a partial month.</div>
<% Else %>
<div class="small-note px-2 pb-1">The trend chart compares <%=trendEarliestYear%> to <%=trendSelectedYear%> and <%=trendCurrentYear%> year to date. <%=Html(MonthName(trendCurrentMonth) & " " & trendCurrentYear)%> is a partial month.</div>
<% End If %>
<% Else %>
<div class="small-note px-2 pb-1">The trend chart groups results by calendar month.</div>
<% End If %>
<% End If %>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<div class="d-flex justify-content-between align-items-center gap-2">
<h2>Country ranking</h2>
<span class="badge text-bg-light">Top <%=tableCount%></span>
</div>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="text-end">#</th>
<th class="country-name">Country</th>
<th>Code</th>
<th class="text-end<% If measure = "orders" Then Response.Write " table-primary" %>">Orders</th>
<th class="text-end<% If measure = "sales" Then Response.Write " table-primary" %>">Sales</th>
<th class="text-end">Share of <%=LCase(valueColumnLabel)%></th>
</tr>
</thead>
<tbody>
<% For i = 0 To tableCount - 1
countryCode = CStr(dataRows(0, i))
countryName = CStr(dataRows(1, i))
currentOrders = CLng(dataRows(2, i))
currentSales = CDbl(dataRows(3, i))
If measure = "orders" Then
currentValue = CDbl(currentOrders)
Else
currentValue = currentSales
End If
shareValue = 0
If totalSelected > 0 Then shareValue = (currentValue / totalSelected) * 100
%>
<tr>
<td class="text-end text-muted"><%=i + 1%></td>
<td class="fw-semibold"><%=Html(countryName)%></td>
<td><span class="badge text-bg-secondary"><%=Html(countryCode)%></span></td>
<td class="text-end"><%=Number0(currentOrders)%></td>
<td class="text-end"><%=Money0(currentSales)%></td>
<td class="text-end"><%=FormatNumber(shareValue, 1)%>%</td>
</tr>
<% Next %>
</tbody>
</table>
</div>
</div>
<% End If %>
</div>
<script>
(function () {
'use strict';
var rangeSelect = document.getElementById('DateRange');
var startInput = document.getElementById('StartDate');
var endInput = document.getElementById('EndDate');
if (!rangeSelect || !startInput || !endInput) return;
function formatDate(date) {
var year = date.getFullYear();
var month = String(date.getMonth() + 1).padStart(2, '0');
var day = String(date.getDate()).padStart(2, '0');
return year + '-' + month + '-' + day;
}
function addDays(date, days) {
var result = new Date(date.getFullYear(), date.getMonth(), date.getDate());
result.setDate(result.getDate() + days);
return result;
}
function addMonths(date, months) {
var day = date.getDate();
var result = new Date(date.getFullYear(), date.getMonth(), 1);
result.setMonth(result.getMonth() + months);
var lastDay = new Date(result.getFullYear(), result.getMonth() + 1, 0).getDate();
result.setDate(Math.min(day, lastDay));
return result;
}
function setCustomMode(isCustom) {
startInput.readOnly = !isCustom;
endInput.readOnly = !isCustom;
startInput.required = isCustom;
endInput.required = isCustom;
}
function applySelectedRange() {
var today = new Date();
var value = rangeSelect.value;
var start = null;
var end = today;
if (value === 'custom') {
setCustomMode(true);
if (!startInput.value) startInput.value = formatDate(addMonths(today, -12));
if (!endInput.value) endInput.value = formatDate(today);
return;
}
setCustomMode(false);
switch (value) {
case '30days': start = addDays(today, -29); break;
case '60days': start = addDays(today, -59); break;
case '90days': start = addDays(today, -89); break;
case 'thisquarter':
start = new Date(today.getFullYear(), Math.floor(today.getMonth() / 3) * 3, 1);
break;
case 'previousquarter':
end = new Date(today.getFullYear(), Math.floor(today.getMonth() / 3) * 3, 0);
start = new Date(end.getFullYear(), end.getMonth() - 2, 1);
break;
case '6months': start = addMonths(today, -6); break;
case '12months': start = addMonths(today, -12); break;
case 'yeartodate':
start = new Date(today.getFullYear(), 0, 1);
break;
case 'lastyear':
start = new Date(today.getFullYear() - 1, 0, 1);
end = new Date(today.getFullYear() - 1, 11, 31);
break;
case '2years': start = addMonths(today, -24); break;
case '3years': start = addMonths(today, -36); break;
case '5years': start = addMonths(today, -60); break;
case 'all':
startInput.value = '';
endInput.value = formatDate(today);
return;
default:
start = addMonths(today, -12);
}
startInput.value = formatDate(start);
endInput.value = formatDate(end);
}
rangeSelect.addEventListener('change', applySelectedRange);
}());
</script>
<% If validationMessage = "" And dataError = "" And rowCount > 0 Then %>
<script>
(function () {
'use strict';
var currentView = '<%=JsString(chartView)%>';
var currentMeasure = '<%=JsString(measure)%>';
var currentTrendInterval = '<%=JsString(trendInterval)%>';
var currentCalendarYear = <%=Year(Date())%>;
var chartElement = document.getElementById('sales-chart');
var loadingElement = document.getElementById('chart-loading');
var resizeTimer = null;
var chartsLoaded = false;
var comparisonPreviousYears = <%=previousYears%>;
function buildComparisonSeriesOptions() {
var series = {};
var i;
for (i = 0; i < comparisonPreviousYears; i += 1) {
series[i] = {lineWidth: 2, pointSize: 3};
}
series[comparisonPreviousYears] = {lineWidth: 4, pointSize: 5};
series[comparisonPreviousYears + 1] = {lineWidth: 3, pointSize: 5, lineDashStyle: [8, 4]};
return series;
}
function buildComparisonColors() {
var historicalPalette = ['#8e44ad', '#16a085', '#c0392b', '#2980b9', '#f39c12', '#27ae60', '#d35400', '#e83e8c', '#6f42c1', '#17a2b8'];
var colors = [];
var startIndex = historicalPalette.length - comparisonPreviousYears;
var i;
for (i = startIndex; i < historicalPalette.length; i += 1) {
colors.push(historicalPalette[i]);
}
colors.push('#0d6efd');
colors.push('#fd7e14');
return colors;
}
google.charts.load('current', {
packages: ['geochart', 'corechart'],
language: 'en-GB'
});
google.charts.setOnLoadCallback(function () {
chartsLoaded = true;
drawSalesChart();
});
function formatValueColumns(data, columnIndexes) {
var formatter;
var i;
if (currentMeasure === 'orders') {
formatter = new google.visualization.NumberFormat({fractionDigits: 0});
} else {
formatter = new google.visualization.NumberFormat({prefix: '£', fractionDigits: 0});
}
for (i = 0; i < columnIndexes.length; i += 1) {
formatter.format(data, columnIndexes[i]);
}
}
function addFormatting(data) {
formatValueColumns(data, [1]);
}
function buildMapData() {
var data = new google.visualization.DataTable();
data.addColumn('string', 'Country');
data.addColumn('number', '<%=JsString(valueColumnLabel)%>');
<% For i = 0 To rowCount - 1
countryCode = CStr(dataRows(0, i))
countryName = CStr(dataRows(1, i))
If measure = "orders" Then
currentValue = CDbl(dataRows(2, i))
Else
currentValue = CDbl(dataRows(3, i))
End If
%>
data.addRow([{v: '<%=JsString(countryCode)%>', f: '<%=JsString(countryName)%>'}, <%=JsNumber(currentValue)%>]);
<% Next %>
addFormatting(data);
return data;
}
function buildBarData() {
var data = new google.visualization.DataTable();
data.addColumn('string', 'Country');
data.addColumn('number', '<%=JsString(valueColumnLabel)%>');
<% For i = 0 To barCount - 1
countryName = CStr(dataRows(1, i))
If measure = "orders" Then
currentValue = CDbl(dataRows(2, i))
Else
currentValue = CDbl(dataRows(3, i))
End If
%>
data.addRow(['<%=JsString(countryName)%>', <%=JsNumber(currentValue)%>]);
<% Next %>
addFormatting(data);
return data;
}
function buildPieData() {
var data = new google.visualization.DataTable();
data.addColumn('string', 'Country');
data.addColumn('number', '<%=JsString(valueColumnLabel)%>');
<% For i = 0 To pieCount - 1
countryName = CStr(dataRows(1, i))
If measure = "orders" Then
currentValue = CDbl(dataRows(2, i))
Else
currentValue = CDbl(dataRows(3, i))
End If
%>
data.addRow(['<%=JsString(countryName)%>', <%=JsNumber(currentValue)%>]);
<% Next %>
<% If pieOtherValue > 0 Then %>
data.addRow(['Other', <%=JsNumber(pieOtherValue)%>]);
<% End If %>
addFormatting(data);
return data;
}
function buildTrendData() {
var data = new google.visualization.DataTable();
<% If trendInterval = "yearly" Then %>
var yearlyValues = {};
var yearOrder = [];
var yearKey;
var i;
data.addColumn('string', 'Year');
data.addColumn('number', '<%=JsString(valueColumnLabel)%>');
data.addColumn({type: 'string', role: 'style'});
<% For i = 0 To trendRowCount - 1
trendYear = CInt(trendRows(0, i))
If measure = "orders" Then
currentValue = CDbl(trendRows(2, i))
Else
currentValue = CDbl(trendRows(3, i))
End If
%>
yearKey = '<%=trendYear%>';
if (!Object.prototype.hasOwnProperty.call(yearlyValues, yearKey)) {
yearlyValues[yearKey] = 0;
yearOrder.push(yearKey);
}
yearlyValues[yearKey] += <%=JsNumber(currentValue)%>;
<% Next %>
for (i = 0; i < yearOrder.length; i += 1) {
yearKey = yearOrder[i];
data.addRow([
yearKey === String(currentCalendarYear) ? yearKey + ' YTD' : yearKey,
yearlyValues[yearKey],
yearKey === String(currentCalendarYear) ? 'color: #fd7e14' : 'color: #0d6efd'
]);
}
addFormatting(data);
<% ElseIf trendComparisonMode Then %>
data.addColumn('string', 'Month');
<% For trendHistoricalYear = trendEarliestYear To trendSelectedYear %>
data.addColumn('number', '<%=trendHistoricalYear%>');
<% Next %>
data.addColumn('number', '<%=trendCurrentYear%> year to date');
data.addColumn({type: 'string', role: 'annotation'});
<% For i = 1 To 12 %>
data.addRow([
'<%=MonthName(i, True)%>',
<% For trendYearIndex = 0 To previousYears %>
<%=JsNumber(trendHistoricalValues(trendYearIndex, i))%>,
<% Next %>
<% If i <= trendCurrentMonth Then %><%=JsNumber(trendCurrentValues(i))%><% Else %>null<% End If %>,
<% If i = trendCurrentMonth Then %>'Partial'<% Else %>null<% End If %>
]);
<% Next %>
formatValueColumns(data, [<% For i = 1 To previousYears + 2 %><% If i > 1 Then Response.Write ", " %><%=i%><% Next %>]);
<% Else %>
data.addColumn('date', 'Month');
data.addColumn('number', '<%=JsString(valueColumnLabel)%>');
<% For i = 0 To trendRowCount - 1
trendYear = CInt(trendRows(0, i))
trendMonth = CInt(trendRows(1, i))
If measure = "orders" Then
currentValue = CDbl(trendRows(2, i))
Else
currentValue = CDbl(trendRows(3, i))
End If
%>
data.addRow([new Date(<%=trendYear%>, <%=trendMonth - 1%>, 1), <%=JsNumber(currentValue)%>]);
<% Next %>
new google.visualization.DateFormat({pattern: 'MMM yyyy'}).format(data, 0);
addFormatting(data);
<% End If %>
return data;
}
function hideLoading() {
if (loadingElement) loadingElement.classList.add('d-none');
}
function drawSalesChart() {
if (!chartsLoaded || !chartElement) return;
var width = Math.max(chartElement.clientWidth, 320);
var height;
var data;
var chart;
var options;
if (currentView === 'bar') {
height = Math.max(440, <%=barCount%> * 38 + 90);
chartElement.style.height = height + 'px';
data = buildBarData();
chart = new google.visualization.BarChart(chartElement);
options = {
width: width,
height: height,
backgroundColor: 'transparent',
legend: 'none',
chartArea: {left: 180, top: 25, width: '70%', height: '86%'},
hAxis: {
minValue: 0,
format: currentMeasure === 'orders' ? '#,##0' : '£#,##0',
textStyle: {fontSize: 12}
},
vAxis: {textStyle: {fontSize: 13}},
colors: ['#0d6efd']
};
} else if (currentView === 'pie') {
height = Math.max(480, Math.min(620, Math.round(width * 0.56)));
chartElement.style.height = height + 'px';
data = buildPieData();
chart = new google.visualization.PieChart(chartElement);
options = {
width: width,
height: height,
backgroundColor: 'transparent',
chartArea: {left: 20, top: 25, width: '88%', height: '86%'},
legend: {position: width < 700 ? 'bottom' : 'right', textStyle: {fontSize: 13}},
pieSliceText: 'percentage',
sliceVisibilityThreshold: 0,
tooltip: {text: 'both'},
colors: ['#0d6efd', '#dc3545', '#fd7e14', '#198754', '#6f42c1', '#0dcaf0', '#ffc107', '#6610f2', '#20c997', '#6c757d', '#adb5bd']
};
} else if (currentView === 'trend') {
height = Math.max(480, Math.min(720, Math.round(width * 0.50) + <% If trendComparisonMode And trendInterval = "monthly" Then %>Math.max(0, Math.ceil((comparisonPreviousYears + 2) / 5) - 1) * 30<% Else %>0<% End If %>));
chartElement.style.height = height + 'px';
data = buildTrendData();
chart = currentTrendInterval === 'yearly'
? new google.visualization.ColumnChart(chartElement)
: new google.visualization.LineChart(chartElement);
options = {
width: width,
height: height,
backgroundColor: 'transparent',
legend: <% If trendComparisonMode And trendInterval = "monthly" Then %>{position: 'top', alignment: 'center', maxLines: 3, textStyle: {fontSize: 12}}<% Else %>'none'<% End If %>,
chartArea: {left: width < 700 ? 72 : 95, top: <% If trendComparisonMode And trendInterval = "monthly" Then %>55 + Math.max(0, Math.ceil((comparisonPreviousYears + 2) / 5) - 1) * 24<% Else %>25<% End If %>, width: width < 700 ? '76%' : '84%', height: <% If trendComparisonMode And trendInterval = "monthly" Then Response.Write "64" Else Response.Write "72" %> + '%'},
hAxis: {
<% If trendInterval = "monthly" And Not trendComparisonMode Then %>format: 'MMM yyyy',
gridlines: {count: Math.min(12, Math.max(4, <%=trendRowCount%>))},
minorGridlines: {count: 0},<% End If %>
textStyle: {fontSize: 12}
},
vAxis: {
minValue: 0,
format: currentMeasure === 'orders' ? '#,##0' : '£#,##0',
textStyle: {fontSize: 12}
},
<% If trendInterval = "yearly" Then %>
bar: {groupWidth: '62%'},
colors: ['#0d6efd'],
<% ElseIf trendComparisonMode Then %>
series: buildComparisonSeriesOptions(),
annotations: {
style: 'point',
textStyle: {fontSize: 11, auraColor: 'none'}
},
colors: buildComparisonColors(),
interpolateNulls: false,
<% Else %>
lineWidth: 3,
pointSize: <% If trendRowCount <= 36 Then Response.Write 5 Else Response.Write 2 %>,
colors: ['#0d6efd'],
<% End If %>
focusTarget: 'category'
};
} else {
height = currentView === 'europe'
? Math.max(480, Math.min(720, Math.round(width * 0.62)))
: Math.max(460, Math.min(680, Math.round(width * 0.52)));
chartElement.style.height = height + 'px';
data = buildMapData();
chart = new google.visualization.GeoChart(chartElement);
options = {
width: width,
height: height,
region: currentView === 'europe' ? '150' : 'world',
backgroundColor: '#eaf6fd',
datalessRegionColor: '#f3f4f6',
defaultColor: '#f3f4f6',
colorAxis: {minValue: 0, colors: ['#d9f4d2', '#6aaa5d', '#1f6417']},
legend: {textStyle: {fontSize: 12}},
tooltip: {textStyle: {fontSize: 13}}
};
}
google.visualization.events.addListener(chart, 'ready', hideLoading);
chart.draw(data, options);
}
window.addEventListener('resize', function () {
if (!chartsLoaded) return;
window.clearTimeout(resizeTimer);
resizeTimer = window.setTimeout(drawSalesChart, 180);
});
}());
</script>
<% End If %>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
</body>
</html>