var
params
=
new
URLSearchParams
( window
. location
. search
);
var
data
=
null
;
var
options
=
null
;
var
proxyTable
=
null
;
let
proxyDataTable
=
null
;
var
chart0
=
null
;
var
filters
=
[];
//variables for showing/hiding columns
var
activeColumns
=
[];
let
chartSelectListener
=
false
; //prevent duplicate listeners from being added
$
( function
() {
google
. charts
. load
( 'current'
, { 'packages'
:
[ 'corechart'
, 'controls'
]});
google
. charts
. setOnLoadCallback
( function
() {
getData
()
. done
( function
( response
) { //runs once response is returned from getData()
let
processedData
=
processData
( response
); //add new rows (Fall, Spring, Academic Year)
drawChart
( processedData
);
})
. fail
( function
( jqXHR
, textStatus
, errorThrown
) {
console
. error
( "Error fetching data:"
, textStatus
, errorThrown
);
$
( '#loading'
). text
( "Error loading data."
); // Update loading message on error
})
. always
( function
() {
$
( '#loading'
). remove
(); // Remove loading indicator once done (success or fail)
});
});
$
( '#contents'
). append
( '<div style="display: none;" id="proxyTable"></div>'
); //add the proxyTable to the DOM because it is necessary for Google Charts to operate properly
//make the chart auto resize based on window size
$
( window
). resize
( function
() {
if
( this
. resizeTO
) clearTimeout
( this
. resizeTO
);
this
. resizeTO
=
setTimeout
( function
() {
$
( this
). trigger
( 'resizeEnd'
);
}, 500
);
});
//redraw graph when window resize is completed
$
( window
). on
( 'resizeEnd'
, function
( e
) {
chart0
. draw
();
});
});
function
getData
() {
$
( '#ef_left'
). append
( '<div id="loading" style="width: 100%; text-align: center;">Loading</div>'
);
const
baseDataURL
=
window
. location
. pathname
. replace
( /index.php.
*
$
/
, 'index.php/json/'
);
let
data
=
$
. getJSON
( baseDataURL
+
'/data'
, $
. param
({ 'func'
:
'classsections'
, 'json'
:
''
}));
return
data
;
}
//add a column for the Period(Academic Year, Fall, or Spring) and Year for filtering and remove null/empty values for any columns with number as the type
function
processData
( rawData
) {
const
transformedData
=
{
cols:
rawData
. cols
. filter
( col
=>
col
. label
!==
"Academic Year"
),
rows:
[]
};
//add the two columns to the beginning
transformedData
. cols
. unshift
(
{ id:
""
, label:
"Period"
, pattern:
""
, type:
"string"
},
{ id:
""
, label:
"Year"
, pattern:
""
, type:
"string"
}
);
const
academicYearIndex
=
rawData
. cols
. findIndex
( col
=>
col
. label
===
"Academic Year"
);
const
termIndex
=
rawData
. cols
. findIndex
( col
=>
col
. label
===
"Term"
);
rawData
. rows
. forEach
( row
=>
{
const
term
=
row
. c
[ termIndex
]?. v
||
""
;
const
academicYear
=
printAYperiod
( row
. c
[ academicYearIndex
]?. v
||
""
);
const
baseRow
=
row
. c
. filter
(( _
, idx
) =>
idx
!==
academicYearIndex
);
transformedData
. rows
. push
({
c:
[
{ v:
"Academic Year"
, f:
null
},
{ v:
academicYear
, f:
null
},
...
baseRow
]
});
if
( term
. endsWith
( "70"
)) {
transformedData
. rows
. push
({
c:
[
{ v:
"Fall"
, f:
null
},
{ v:
term
. substring
( 0
, 4
), f:
null
},
...
baseRow
]
});
}
if
( term
. endsWith
( "20"
)) {
transformedData
. rows
. push
({
c:
[
{ v:
"Spring"
, f:
null
},
{ v:
term
. substring
( 0
, 4
), f:
null
},
...
baseRow
]
});
}
});
//after manipulating the data, make sure to resolve any null values for numbers
//identify numeric column indexes
const
numericIndexes
=
transformedData
. cols
. map
(( col
, idx
) =>
col
. type
===
"number"
?
idx
:
-
1
)
. filter
( idx
=>
idx
!==
-
1
);
//iterate over all rows
transformedData
. rows
. forEach
( row
=>
{
numericIndexes
. forEach
( idx
=>
{
const
cell
=
row
. c
[ idx
];
const
value
=
cell
?. v
;
//if cell missing, null, empty string, or invalid — set to 0
if
( !
cell
||
cell
. v
===
null
||
cell
. v
===
""
||
isNaN
( cell
. v
)) {
row
. c
[ idx
] =
{ v:
0
, f:
null
};
} else
{
//force all to type number just in case any were saved as strings
row
. c
[ idx
]. v
=
Number
( value
);
}
});
});
return
transformedData
;
}
function
drawChart
( d
) {
data
=
new
google
. visualization
. DataTable
( d
); //data is transformed to a Google dataTable
//desired filters (must be a column in the returned data)
const
filterList
=
[
{ name:
'Period'
, allowMultiple:
false
, allowNone:
false
},
{ name:
'Campus'
, allowMultiple:
true
, allowNone:
true
},
{ name:
'College'
, allowMultiple:
true
, allowNone:
true
},
{ name:
'Department'
, allowMultiple:
true
, allowNone:
true
},
{ name:
'Course Level'
, allowMultiple:
true
, allowNone:
true
},
{ name:
'Detail Level'
, allowMultiple:
true
, allowNone:
true
},
{ name:
'Concurrent'
, allowMultiple:
false
, allowNone:
true
}, //don't allow multiple because if both Y and N are selected it is the same as not selecting either one
{ name:
'General Education'
, allowMultiple:
false
, allowNone:
true
} //don't allow multiple because if both Y and N are selected it is the same as not selecting either one
];
//create the array of filters containing the Google controlWrappers
filterList
. forEach
( filterInfo
=>
{
let
filter_id
=
filterInfo
. name
. replace
( / /
g
, "_"
) +
'_filter'
; //replace all spaces with _ and append _filter
$
( "#ef_right"
). append
( '<div id="'
+
filter_id
+
'"></div>'
); //create all filters in the ef_right div
filters
. push
( new
google
. visualization
. ControlWrapper
({
controlType:
'CategoryFilter'
,
containerId:
filter_id
,
options:
{
filterColumnLabel:
filterInfo
. name
, //this is the label from your dataTable/the returned column names from the database
ui:
{
allowMultiple:
filterInfo
. allowMultiple
,
allowTyping:
false
,
sortValues:
true
,
selectedValuesLayout:
'below'
,
labelStacking:
'vertical'
,
allowNone:
filterInfo
. allowNone
}
}
}));
});
//invisible table used for filtering
proxyTable
=
new
google
. visualization
. ChartWrapper
({
'chartType'
:
'Table'
,
'containerId'
:
'proxyTable'
});
//build the chartWrapper for the column graph
options
=
{
'hAxis'
:
{
'title'
:
null
},
'vAxis'
:
{
'format'
:
'#,###'
,
'minValue'
:
0
,
'maxValue'
:
'auto'
,
'textPosition'
:
'in'
,
'title'
:
null
},
//'title': 'Class Sections and SSCH',
'legend'
:
{
'position'
:
'bottom'
},
'chartArea'
:
{
'left'
:
'10'
,
'right'
:
'10'
,
'top'
:
'50'
,
'bottom'
:
'50'
,
'width'
:
'100%'
,
'height'
:
'100%'
,
},
'width'
:
'100%'
,
'height'
:
450
,
'viewWindowMode'
:
'pretty'
,
'tooltip'
:
{
'trigger'
:
'selection'
,
'isHtml'
:
true
},
'annotations'
:
{
'alwaysOutside'
:
true
,
'highContrast'
:
true
,
}
};
options
=
JSON
. parse
( JSON
. stringify
( options
));
chart0
=
new
google
. visualization
. ChartWrapper
({
'chartType'
:
'ColumnChart'
,
'containerId'
:
'chart0'
,
'options'
:
options
});
var
dash
=
new
google
. visualization
. Dashboard
( document
. getElementById
( 'dashboard'
));
dash
. bind
( filters
, proxyTable
);
google
. visualization
. events
. addListener
( proxyTable
, 'ready'
, proxyTableReady
); //call proxyTableReady once the proxy table is fully drawn
dash
. draw
( data
); //draw the dashboard with retrieved JSON data which has been converted to a google dataTable
}
function
proxyTableReady
() {
proxyDataTable
=
proxyTable
. getDataTable
(); //retrieve filtered datatable (filters are applied to proxyTable)
//get indexes and then aggregate/group data for graphing
let
yearIndex
=
proxyDataTable
. getColumnIndex
( 'Year'
); //23
let
sschIndex
=
proxyDataTable
. getColumnIndex
( 'SSCH'
); //0
let
enrollmentsIndex
=
proxyDataTable
. getColumnIndex
( 'ENROLLMENTS'
); //1
let
crnIndex
=
proxyDataTable
. getColumnIndex
( 'CRN'
); //2
});
chartSelectListener
=
true
; //prevent the listener from being added more than once
}
}
//makes the chart downloadable by adding a download button to the tooltip.
function
setChartActionDownload
( chart
) {
chart
. getChart
(). setAction
({
'id'
:
chart
. getContainerId
() +
'_tooltip'
,
'text'
:
'Download Data'
,
'action'
:
function
() { downloadData
( chart
); },
'visible'
:
function
() { return
isDownloadVisible
( chart
); }
});
}
function
downloadData
( chart
) {
var
chartData
=
chart
. getDataTable
();
var
selection
=
chart
. getChart
(). getSelection
();
var
filter_data
=
{};
//in the future it could limit the download to the single academic year that is clicked on
//filter_data['field'] = chartData.getColumnLabel(selection[0]['column']); //grabs the name of the column the user clicked in Google Charts
//grab the filter options
$
( filters
). each
( function
() {
filter_data
[ this
. getOption
( 'filterColumnLabel'
)] =
this
. getState
()[ 'selectedValues'
];
});
window
. location
. href
=
'classsections/csv?'
+
$
. param
( filter_data
);
/* this whole section will download the data directly from the dataTable
//manually build the header row
let headerRow = [];
for (let i = 0; i < proxyDataTable.getNumberOfColumns(); i++) {
let label = proxyDataTable.getColumnLabel(i);
headerRow.push('"' + label.replace(/"/g, '""') + '"');
}
let csvHeaders = headerRow.join(',') + '\n';
let csvFormattedDataRows = google.visualization.dataTableToCsv(proxyDataTable);
//combine headers and data
let fullCsvContent = csvHeaders + csvFormattedDataRows;
let encodedUri = 'data:application/csv;charset=utf-8,' + encodeURIComponent(fullCsvContent);
//download link creation
let $link = $('<a>')
.attr('href', encodedUri)
.attr('download', 'class_sections_ssch.csv')
.css('display', 'none')
.appendTo('body');
$link[0].click(); //execute the downloading by "clicking" the link
$link.remove(); //clean up the DOM
*/
}
function
isDownloadVisible
( chart
) {
let
chartData
=
chart
. getDataTable
();
let
selection
=
chart
. getChart
(). getSelection
();
if
( selection
. length
>
0
) {
if
( chartData
. getColumnRole
( selection
[ 0
][ 'column'
]) ==
'annotation'
) //prevent the download button from showing on columns with the role of annotation
return
false
;
else
return
true
;
} else
return
false
;
}
//returns the period of years when given the academic year
function
printAYperiod
( academicYear
) {
const
endYear
=
parseInt
( academicYear
, 10
); //convert to integer
if
( !
isNaN
( endYear
)) { //check if conversion was successful
const
startYear
=
endYear
-
1
;
return
`
${
startYear
}
-
${
endYear
}
`
;
} else
{
return
false
;
}
}
//inserts a value into an array in numerical order
//the array is assumed to consist of numbers
function
insertSorted
( arr
, val
) {
let
i
=
0
;
while
( i
<
arr
. length
&&
arr
[ i
] <
val
) {
i
++
;
}
arr
. splice
( i
, 0
, val
); // Insert val at index i