chore: start adding overview page

main
Nelson Jovel 2 years ago
parent 1b0af124f7
commit 64b4d599c7

@ -0,0 +1,8 @@
# frozen_string_literal: true
class AnalyzeController < SqmApplicationController
def index
@presenter = Analyze::Presenter.new(params:, school: @school, academic_year: @academic_year)
@background ||= BackgroundPresenter.new(num_of_columns: @presenter.graph.columns.count)
end
end

@ -0,0 +1,11 @@
# frozen_string_literal: true
class CategoriesController < SqmApplicationController
helper GaugeHelper
def show
@categories = Category.sorted.map { |category| CategoryPresenter.new(category:) }
@category = CategoryPresenter.new(category: Category.find_by_slug(params[:id]))
end
end

@ -0,0 +1,8 @@
class GpsController < ActionController::Base
def index
# respond_to do |format|
# format.html
# format.csv { send_data Report::Gps.to_csv, disposition: 'attachment', filename: "gps_#{Date.today}.csv" }
# end
end
end

@ -49,7 +49,7 @@ module Dashboard
academic_year = AcademicYear.all.order(range: :DESC).find do |ay|
Subcategory.all.any? do |subcategory|
rate = subcategory.response_rate(school:, academic_year: ay)
rate.meets_student_threshold || rate.meets_teacher_threshold
rate.meets_student_threshold? || rate.meets_teacher_threshold?
end
end

@ -0,0 +1,44 @@
# frozen_string_literal: true
module Dashboard
class OverviewController < SqmApplicationController
before_action :check_empty_dataset, only: [:index]
helper VarianceHelper
def index
@variance_chart_row_presenters = measures.map(&method(:presenter_for_measure))
@category_presenters = categories.map { |category| CategoryPresenter.new(category:) }
@student_response_rate_presenter = ResponseRatePresenter.new(focus: :student, school: @school,
academic_year: @academic_year)
@teacher_response_rate_presenter = ResponseRatePresenter.new(focus: :teacher, school: @school,
academic_year: @academic_year)
end
private
def presenter_for_measure(measure)
score = measure.score(school: @school, academic_year: @academic_year)
VarianceChartRowPresenter.new(measure:, score:)
end
def check_empty_dataset
@has_empty_dataset = subcategories.none? do |subcategory|
response_rate = subcategory.response_rate(school: @school, academic_year: @academic_year)
response_rate.meets_student_threshold? || response_rate.meets_teacher_threshold?
end
end
def measures
@measures ||= subcategories.flat_map(&:measures)
end
def subcategories
@subcategories ||= categories.flat_map(&:subcategories)
end
def categories
@categories ||= Category.sorted.includes(%i[measures scales admin_data_items subcategories])
end
end
end

@ -0,0 +1,3 @@
class ReportsController < ApplicationController
def index; end
end

@ -0,0 +1,44 @@
# frozen_string_literal: true
module Dashboard
class SqmApplicationController < ApplicationController
protect_from_forgery with: :exception, prepend: true
before_action :set_schools_and_districts
before_action :authenticate_district
helper HeaderHelper
private
def authenticate_district
authenticate(district_name, "#{district_name}!")
end
def district_name
@district_name ||= @district.name.split(" ").first.downcase
end
def set_schools_and_districts
@district = District.find_by_slug district_slug
@districts = District.all.order(:name)
@school = School.find_by_slug(school_slug)
@schools = School.includes([:district]).where(district: @district).order(:name)
@academic_year = AcademicYear.find_by_range params[:year]
@academic_years = AcademicYear.all.order(range: :desc)
end
def district_slug
params[:district_id]
end
def school_slug
params[:school_id]
end
def authenticate(username, password)
authenticate_or_request_with_http_basic do |u, p|
u == username && p == password
end
end
end
end

@ -0,0 +1,70 @@
# frozen_string_literal: true
module Dashboard
module AnalyzeHelper
def svg_height
400
end
def zone_label_width
15
end
def graph_width
85
end
def analyze_graph_height
85
end
def analyze_zone_height
analyze_graph_height / 5
end
def zone_height_percentage
analyze_zone_height / 100.0
end
def analyze_category_link(district:, school:, academic_year:, category:)
year = academic_year.range
"/districts/#{district.slug}/schools/#{school.slug}/analyze?year=#{year}&academic_years=#{year}&category=#{category.category_id}"
end
def analyze_subcategory_link(district:, school:, academic_year:, category:, subcategory:)
"/districts/#{district.slug}/schools/#{school.slug}/analyze?year=#{academic_year.range}&category=#{category.category_id}&subcategory=#{subcategory.subcategory_id}"
end
def colors
@colors ||= ["#49416D", "#FFC857", "#920020", "#00B0B3", "#B2D236", "#004D61", "#FFB3CC", "#FF3D00", "#212121", "#9E9D24",
"#689F38", "#388E3C", "#00897B", "#00796B", "#00695C", "#004D40", "#1B5E20", "#FF6F00", "#33691E", "#D50000",
"#827717", "#F57F17", "#FF6F00", "#E65100", "#BF360C", "#3E2723", "#263238", "#37474F", "#455A64"]
end
def empty_dataset?(measures:, school:, academic_year:)
@empty_dataset ||= Hash.new do |memo, (school, academic_year)|
memo[[school, academic_year]] = measures.none? do |measure|
response_rate = measure.subcategory.response_rate(school:, academic_year:)
response_rate.meets_student_threshold || response_rate.meets_teacher_threshold || measure.sufficient_admin_data?(school:, academic_year:)
end
end
@empty_dataset[[school, academic_year]]
end
def empty_survey_dataset?(measures:, school:, academic_year:)
@empty_survey_dataset ||= Hash.new do |memo, (school, academic_year)|
memo[[school, academic_year]] = measures.none? do |measure|
response_rate = measure.subcategory.response_rate(school:, academic_year:)
response_rate.meets_student_threshold || response_rate.meets_teacher_threshold
end
end
@empty_survey_dataset[[school, academic_year]]
end
def base_url
analyze_subcategory_link(district: @district, school: @school, academic_year: @academic_year, category: @presenter.category,
subcategory: @presenter.subcategory)
end
end
end

@ -0,0 +1,99 @@
# frozen_string_literal: true
Point = Struct.new(:x, :y)
Rect = Struct.new(:x, :y, :width, :height)
module GaugeHelper
def outer_radius
100
end
def inner_radius
50
end
def stroke_width
1
end
def effective_radius
outer_radius + stroke_width
end
def diameter
2 * effective_radius
end
def width
diameter
end
def height
outer_radius + 2 * stroke_width + key_benchmark_indicator_gutter
end
def key_benchmark_indicator_gutter
10
end
def viewbox
x = arc_center.x - effective_radius
y = arc_center.y - effective_radius - key_benchmark_indicator_gutter
Rect.new(x, y, width, height)
end
def arc_center
Point.new(0, 0)
end
def arc_radius(radius)
"#{radius} #{radius}"
end
def angle_for(percentage:)
-Math::PI * (1 - percentage)
end
def arc_end_point_for(radius:, percentage:)
angle = angle_for(percentage:)
x = arc_center.x + radius * Math.cos(angle)
y = arc_center.y + radius * Math.sin(angle)
Point.new(x, y)
end
def arc_end_line_destination(radius:, percentage:)
angle = angle_for(percentage:)
x = arc_center.x + radius * Math.cos(angle)
y = arc_center.y + radius * Math.sin(angle)
Point.new(x, y)
end
def arc_start_point
Point.new(arc_center.x - outer_radius, arc_center.y)
end
def move_to(point:)
"M #{coordinates_for(point)}"
end
def draw_arc(radius:, percentage:, clockwise:)
sweep_flag = clockwise ? 1 : 0
"A #{arc_radius(radius)} 0 0 #{sweep_flag} #{coordinates_for(arc_end_point_for(radius:,
percentage:))}"
end
def draw_line_to(point:)
"L #{coordinates_for(point)}"
end
def benchmark_line_point(radius, angle)
x = (radius * Math.cos(angle)).to_s
y = (radius * Math.sin(angle) + arc_center.y).to_s
Point.new(x, y)
end
def coordinates_for(point)
"#{point.x} #{point.y}"
end
end

@ -0,0 +1,54 @@
# frozen_string_literal: true
module Dashboard
module VarianceHelper
def heading_gutter
30
end
def footer_gutter
50
end
def measure_row_height
40
end
def graph_height(number_of_rows)
number_of_rows * measure_row_height + heading_gutter + footer_gutter
end
def graph_background_height(number_of_rows:)
number_of_rows += 1 if @has_empty_dataset
graph_height(number_of_rows) - footer_gutter
end
def measure_row_bar_height
20
end
def label_width_percentage
30
end
def graph_width_percentage
100 - label_width_percentage
end
def zones
%w[warning watch growth approval ideal]
end
def zone_width_percentage
100.0 / zones.size
end
def availability_indicator_percentage
3
end
def partial_data_indicator_size
20
end
end
end

@ -37,6 +37,5 @@ module Dashboard
private_class_method :academic_years
private_class_method :parse_year_range
end
AcademicYearRange = Struct.new(:start, :end)
end
AcademicYearRange = Struct.new(:start, :end)

@ -1,11 +1,11 @@
module Dashboard
class Measure < ApplicationRecord
belongs_to :subcategory, class_name: "Subcategory", foreign_key: :dashboard_subcategory_id
has_one :dashboard_category, through: :dashboard_subcategory
has_many :dashboard_scales
has_many :dashboard_admin_data_items, through: :scales
has_many :dashboard_survey_items, through: :scales
has_many :dashboard_survey_item_responses, through: :survey_items
has_one :dashboard_category, through: :subcategory
has_many :scales, class_name: "Scale", foreign_key: :dashboard_measure_id
has_many :admin_data_items, through: :scales
has_many :survey_items, through: :scales
has_many :survey_item_responses, through: :survey_items
def none_meet_threshold?(school:, academic_year:)
@none_meet_threshold ||= Hash.new do |memo, (school, academic_year)|
@ -24,15 +24,17 @@ module Dashboard
end
def student_survey_items_with_sufficient_responses(school:, academic_year:)
@student_survey_items_with_sufficient_responses ||= SurveyItem.where(id: SurveyItem.joins("inner join survey_item_responses on survey_item_responses.survey_item_id = survey_items.id")
.student_survey_items
.where("survey_item_responses.school": school,
"survey_item_responses.academic_year": academic_year,
"survey_item_responses.survey_item_id": survey_items.student_survey_items,
"survey_item_responses.grade": school.grades(academic_year:))
.group("survey_items.id")
.having("count(*) >= 10")
.count.keys)
# @student_survey_items_with_sufficient_responses ||= SurveyItem.where(id: SurveyItem.joins("inner join dashboard_survey_item_responses on dashboard_survey_item_responses.survey_item_id = dashboard_survey_items.id")
# .student_survey_items
# .where("dashboard_survey_item_responses.school": school,
# "dashboard_survey_item_responses.academic_year": academic_year,
# "dashboard_survey_item_responses.survey_item_id": survey_items.student_survey_items,
# "dashboard_survey_item_responses.grade": school.grades(academic_year:))
# .group("survey_items.id")
# .having("count(*) >= 10")
# .count.keys)
@student_survey_items_with_sufficient_responses ||= student_survey_items
end
def teacher_scales

@ -1,7 +1,18 @@
module Dashboard
class ResponseRate < ApplicationRecord
belongs_to :dashboard_subcategory
belongs_to :school
belongs_to :dashboard_academic_year
TEACHER_RATE_THRESHOLD = 24.5
STUDENT_RATE_THRESHOLD = 24.5
belongs_to :subcategory, class_name: "Subcategory", foreign_key: :dashboard_subcategory_id
belongs_to :school, class_name: "School", foreign_key: :dashboard_school_id
belongs_to :academic_year, class_name: "AcademicYear", foreign_key: :dashboard_academic_year_id
def meets_student_threshold?
student_response_rate >= 24.5
end
def meets_teacher_threshold?
teacher_response_rate >= 24.5
end
end
end

@ -2,8 +2,6 @@
module Dashboard
class ResponseRateCalculator
TEACHER_RATE_THRESHOLD = 25
STUDENT_RATE_THRESHOLD = 25
attr_reader :subcategory, :school, :academic_year
def initialize(subcategory:, school:, academic_year:)
@ -22,14 +20,6 @@ module Dashboard
cap_at_one_hundred(raw_response_rate).round
end
def meets_student_threshold?
rate >= STUDENT_RATE_THRESHOLD
end
def meets_teacher_threshold?
rate >= TEACHER_RATE_THRESHOLD
end
private
def cap_at_one_hundred(response_rate)

@ -1,9 +1,9 @@
module Dashboard
class Scale < ApplicationRecord
belongs_to :measure, class_name: "Measure", foreign_key: :dashboard_measure_id
has_many :survey_items
has_many :survey_items, class_name: "SurveyItem", foreign_key: :dashboard_scale_id
has_many :survey_item_responses, through: :survey_items
has_many :admin_data_items, class_name: "AdminDataItem", foreign_key: :admin_data_item_id
has_many :admin_data_items, class_name: "AdminDataItem", foreign_key: :dashboard_scale_id
def score(school:, academic_year:)
@score ||= Hash.new do |memo, (school, academic_year)|

@ -37,8 +37,7 @@ module Dashboard
student = StudentResponseRateCalculator.new(subcategory: self, school:, academic_year:)
teacher = TeacherResponseRateCalculator.new(subcategory: self, school:, academic_year:)
memo[[school, academic_year]] = ResponseRate.new(school:, academic_year:, subcategory: self, student_response_rate: student.rate,
teacher_response_rate: teacher.rate, meets_student_threshold: student.meets_student_threshold?,
meets_teacher_threshold: teacher.meets_teacher_threshold?)
teacher_response_rate: teacher.rate)
end
@response_rate[[school, academic_year]]

@ -15,26 +15,26 @@ module Dashboard
end
scope :student_survey_items, lambda {
where("survey_items.survey_item_id LIKE 's-%'")
where("dashboard_survey_items.survey_item_id LIKE 's-%'")
}
scope :standard_survey_items, lambda {
where("survey_items.survey_item_id LIKE 's-%-q%'")
where("dashboard_survey_items.survey_item_id LIKE 's-%-q%'")
}
scope :teacher_survey_items, lambda {
where("survey_items.survey_item_id LIKE 't-%'")
where("dashboard_survey_items.survey_item_id LIKE 't-%'")
}
scope :short_form_survey_items, lambda {
where(on_short_form: true)
}
scope :early_education_survey_items, lambda {
where("survey_items.survey_item_id LIKE '%-%-es%'")
where("dashboard_survey_items.survey_item_id LIKE '%-%-es%'")
}
scope :survey_items_for_grade, lambda { |school, academic_year, grade|
includes(:survey_item_responses)
.where("survey_item_responses.grade": grade,
"survey_item_responses.school": school,
"survey_item_responses.academic_year": academic_year).distinct
.where("dashboard_survey_item_responses.grade": grade,
"dashboard_survey_item_responses.school": school,
"dashboard_survey_item_responses.academic_year": academic_year).distinct
}
scope :survey_item_ids_for_grade, lambda { |school, academic_year, grade|
@ -45,9 +45,9 @@ module Dashboard
includes(:survey_item_responses)
.where(
survey_item_id: subcategory.survey_items.pluck(:survey_item_id),
"survey_item_responses.school": school,
"survey_item_responses.academic_year": academic_year,
"survey_item_responses.grade": grade
"dashboard_survey_item_responses.school": school,
"dashboard_survey_item_responses.academic_year": academic_year,
"dashboard_survey_item_responses.grade": grade
)
}

@ -3,16 +3,16 @@ module Dashboard
TEACHER_RESPONSE_THRESHOLD = 2
STUDENT_RESPONSE_THRESHOLD = 10
belongs_to :school
belongs_to :dashboard_survey_item
belongs_to :dashboard_academic_year
belongs_to :school, class_name: "School", foreign_key: :dashboard_school_id
belongs_to :survey_item, class_name: "SurveyItem", foreign_key: :dashboard_survey_item_id
belongs_to :academic_year, class_name: "AcademicYear", foreign_key: :dashboard_academic_year
belongs_to :dashboard_student, optional: true
belongs_to :dashboard_gender, optional: true
belongs_to :dashboard_income, optional: true
belongs_to :dashboard_ell, optional: true
belongs_to :dashboard_sped, optional: true
has_one :dashboard_measure, through: :dashboard_survey_item
has_one :dashboard_measure, through: :survey_item
validates :likert_score, numericality: { greater_than: 0, less_than_or_equal_to: 5 }
@ -61,10 +61,10 @@ module Dashboard
def self.teacher_survey_items_with_sufficient_responses(school:, academic_year:)
@teacher_survey_items_with_sufficient_responses ||= Hash.new do |memo, (school, academic_year)|
hash = SurveyItem.joins("inner join survey_item_responses on survey_item_responses.survey_item_id = survey_items.id")
hash = SurveyItem.joins("inner join dashboard_survey_item_responses on dashboard_survey_item_responses.dashboard_survey_item_id = dashboard_survey_items.id")
.teacher_survey_items
.where("survey_item_responses.school": school, "survey_item_responses.academic_year": academic_year)
.group("survey_items.id")
.where("dashboard_survey_item_responses.dashboard_school_id": school.id, "dashboard_survey_item_responses.dashboard_academic_year_id": academic_year.id)
.group("dashboard_survey_items.id")
.having("count(*) > 0").count
memo[[school, academic_year]] = hash
end
@ -73,9 +73,9 @@ module Dashboard
def self.student_survey_items_with_sufficient_responses_by_grade(school:, academic_year:)
@student_survey_items_with_sufficient_responses_by_grade ||= Hash.new do |memo, (school, academic_year)|
hash = SurveyItem.joins("inner join survey_item_responses on survey_item_responses.survey_item_id = survey_items.id")
hash = SurveyItem.joins("inner join dashboard_survey_item_responses on dashboard_survey_item_responses.dashboard_survey_item_id = dashboard_survey_items.id")
.student_survey_items
.where("survey_item_responses.school": school, "survey_item_responses.academic_year": academic_year)
.where("dashboard_survey_item_responses.dashboard_school_id": school.id, "dashboard_survey_item_responses.dashboard_academic_year_id": academic_year.id)
.group(:grade, :id)
.count
memo[[school, academic_year]] = hash

@ -29,7 +29,7 @@ module Analyze
def sufficient_student_responses?(academic_year:)
return false unless measure.subcategory.response_rate(school:, academic_year:).meets_student_threshold?
number_of_students_for_a_racial_group = SurveyItemResponse.joins("JOIN student_races on survey_item_responses.student_id = student_races.student_id JOIN students on students.id = student_races.student_id").where(
number_of_students_for_a_racial_group = SurveyItemResponse.joins("JOIN student_races on dashboard_survey_item_responses.student_id = student_races.student_id JOIN students on students.id = student_races.student_id").where(
school:, academic_year:
).where("student_races.race_id": race.id).distinct.pluck(:student_id).count
number_of_students_for_a_racial_group >= 10

@ -1,32 +0,0 @@
module Dashboard
class DisaggregationLoader
attr_reader :path
def initialize(path:)
@path = path
initialize_directory
end
def load
data = {}
Dir.glob(Rails.root.join(path, "*.csv")).each do |filepath|
puts filepath
File.open(filepath) do |file|
headers = CSV.parse(file.first).first
file.lazy.each_slice(1000) do |lines|
CSV.parse(lines.join, headers:).map do |row|
values = DisaggregationRow.new(row:, headers:)
data[[values.lasid, values.district, values.academic_year]] = values
end
end
end
end
data
end
def initialize_directory
FileUtils.mkdir_p(path)
end
end
end

@ -1,56 +0,0 @@
module Dashboard
class DisaggregationRow
attr_reader :row, :headers
def initialize(row:, headers:)
@row = row
@headers = headers
end
def district
@district ||= value_from(pattern: /District/i)
end
def academic_year
@academic_year ||= value_from(pattern: /Academic\s*Year/i)
end
def raw_income
@income ||= value_from(pattern: /Low\s*Income/i)
end
def lasid
@lasid ||= value_from(pattern: /LASID/i)
end
def raw_ell
@raw_ell ||= value_from(pattern: /EL Student First Year/i)
end
def ell
@ell ||= begin
value = value_from(pattern: /EL Student First Year/i).downcase
case value
when /lep student 1st year|LEP student not 1st year/i
"ELL"
when /Does not apply/i
"Not ELL"
else
"Unknown"
end
end
end
def value_from(pattern:)
output = nil
matches = headers.select do |header|
pattern.match(header)
end.map { |item| item.delete("\n") }
matches.each do |match|
output ||= row[match]
end
output
end
end
end

@ -1,55 +0,0 @@
# frozen_string_literal: true
module Dashboard
class ResponseRateLoader
def self.reset(schools: School.all, academic_years: AcademicYear.all, subcategories: Subcategory.all)
subcategories.each do |subcategory|
schools.each do |school|
next if test_env? && (school != milford)
academic_years.each do |academic_year|
next if test_env? && (academic_year != test_year)
process_response_rate(subcategory:, school:, academic_year:)
end
end
end
end
private
def self.milford
School.find_by_slug "milford-high-school"
end
def self.test_year
AcademicYear.find_by_range "2020-21"
end
def self.rails_env
@rails_env ||= ENV["RAILS_ENV"]
end
def self.process_response_rate(subcategory:, school:, academic_year:)
student = StudentResponseRateCalculator.new(subcategory:, school:, academic_year:)
teacher = TeacherResponseRateCalculator.new(subcategory:, school:, academic_year:)
response_rate = ResponseRate.find_or_create_by!(subcategory:, school:, academic_year:)
response_rate.update!(student_response_rate: student.rate,
teacher_response_rate: teacher.rate,
meets_student_threshold: student.meets_student_threshold?,
meets_teacher_threshold: teacher.meets_teacher_threshold?)
end
def self.test_env?
rails_env == "test"
end
private_class_method :milford
private_class_method :test_year
private_class_method :rails_env
private_class_method :process_response_rate
private_class_method :test_env?
end
end

@ -0,0 +1,23 @@
<div class="mt-5 school-quality-frameworks">
<% category_presenters.each do |category_presenter| %>
<div class="text-center">
<i class="<%= category_presenter.icon_class %> <%= category_presenter.icon_color_class %> fa-2x"></i>
</div>
<div class="text-center">
<h3 class="sub-header-3">
<%= link_to [@district, @school, category_presenter, { year: @academic_year.range }] do %>
<%= category_presenter.name %>
<% end %>
</h3>
</div>
<p class="body-small text-center m-0"><%= category_presenter.short_description %></p>
<div class="subcategory-card">
<div class="subcategory-card__benchmark-list">
<%= render partial: 'subcategory_card', collection: category_presenter.subcategories(academic_year: @academic_year, school: @school).map(&:subcategory_card_presenter) %>
</div>
</div>
<% end %>
</div>

@ -0,0 +1,14 @@
<div
class="overall-response-rate-container"
data-bs-toggle="popover"
data-bs-trigger="hover focus"
data-bs-content="<%= response_rate_presenter.hover_message %>"
data-bs-placement="top"
>
<div><%= response_rate_presenter.date_message %> </div>
<div style="display: flex; justify-content:space-between; width: 100px;">
<div><%= response_rate_presenter.focus.capitalize %> </div>
<%= render partial: "response_rate_graphic", locals: {response_rate_presenter: response_rate_presenter}, cached: true %>
<div><%= response_rate_presenter.percentage %>% </div>
</div>
</div>

@ -0,0 +1,35 @@
<style>
/*
For some reason, none of the sizing in the pie class works, and it always
fills 100% of the containing frame, so the size has to be dictated by .prog
*/
.prog {
width: 16px;
height: 16px;
position: relative;
border: 1px solid black;
border-radius: 50%;
margin-top: 0.2em;
}
.pie {
aspect-ratio: 1;
display: inline-grid;
place-content: center;
margin: 5px;
font-size: 25px;
font-weight: bold;
font-family: sans-serif;
}
#response-rate-pie-<%= response_rate_presenter.focus %>:before{
content: "";
position: absolute;
border-radius: 50%;
inset: 0;
background: conic-gradient(var(--color-<%= response_rate_presenter.color %>) calc(<%= response_rate_presenter.percentage %>*1%),#0000 0);
}
</style>
<div class="prog">
<div id="response-rate-pie-<%= response_rate_presenter.focus %>" class="pie"></div>
</div>

@ -0,0 +1,7 @@
<div class="subcategory-card__benchmark-item">
<svg class="subcategory-card__circle" width="24" height="24" xmlns="http://www.w3.org/2000/svg" <%= "data-bs-toggle=popover" if subcategory_card.insufficient_data? %> data-bs-placement="top" data-bs-content="This subcategory is not displayed due to limited availability of school admin data and/or low survey response rates.">
<use class="harvey-ball harvey-ball--<%= subcategory_card.color %>" xlink:href="#<%= subcategory_card.harvey_ball_icon %>"></use>
</svg>
<%= link_to(subcategory_card.name, district_school_category_path( @district, @school, subcategory_card.category, {year: @academic_year.range, anchor: "#{subcategory_card.subcategory_id}"})) %>
</div>

@ -0,0 +1,165 @@
<% displayed_presenters = presenters.filter { |p| p.sufficient_data? }.sort %>
<% not_displayed_presenters = presenters - displayed_presenters %>
<% if displayed_presenters.none? %>
<p class="caption mb-5">Note: No measures can be displayed due to limited availability of school admin data and/or low survey response rates.</p>
<% elsif not_displayed_presenters.present? %>
<p class="caption mb-5">Note: The following measures are not displayed due to limited availability of school admin data and/or low survey response rates: <%= not_displayed_presenters.map(&:measure_name).join('; ') %>.</p>
<% end %>
<svg width="100%" height=<%= graph_height(displayed_presenters.size) %> xmlns="http://www.w3.org/2000/svg">
<filter id="inset-shadow" x="-50%" y="-50%" width="200%" height="200%">
<feComponentTransfer in=SourceAlpha>
<feFuncA type="table" tableValues="1 0"/>
</feComponentTransfer>
<feGaussianBlur stdDeviation="4"/>
<feOffset dx="0" dy="0" result="offsetblur"/>
<feFlood flood-color="rgb(62, 58, 56, 0.25)" result="color"/>
<feComposite in2="offsetblur" operator="in"/>
<feComposite in2="SourceAlpha" operator="in"/>
<feMerge>
<feMergeNode in="SourceGraphic"/>
<feMergeNode/>
</feMerge>
</filter>
<svg
id="graph-background"
x="<%= label_width_percentage %>%"
y="0"
width="<%= graph_width_percentage %>%"
height="<%= graph_background_height(number_of_rows: displayed_presenters.size) %>"
filter="url(#inset-shadow)"
>
<g id="zone-headings">
<% zones.each_with_index do |zone, index| %>
<text
class="zone-header"
x="<%= index * zone_width_percentage + zone_width_percentage / 2.0 %>%"
y="<%= heading_gutter / 2 %>"
text-anchor="middle"
dominant-baseline="middle"
>
<%= zone.capitalize %>
</text>
<% end %>
</g>
<g id="zone-background" transform="translate(0, <%= heading_gutter %>)">
<% zones.each_with_index do |zone, index| %>
<rect
id="<%= zone %>-zone"
class="zone-background bg-fill-<%= zone %>"
x="<%= index * zone_width_percentage %>%"
y="0"
width="<%= zone_width_percentage %>%"
height="100%"
stroke="#CECECE"
stroke-width="1"
/>
<% end %>
</g>
</svg>
<g id="measure-rows">
<svg id=measure-row-limited-availability-indicator x="0" y="<%= heading_gutter %>">
<% displayed_presenters.each_with_index do |presenter, index| %>
<% if presenter.show_partial_data_indicator? %>
<foreignObject
width="<%= partial_data_indicator_size %>"
height="<%= partial_data_indicator_size %>"
x="<%= partial_data_indicator_size / 2 %>"
y="<%= index * measure_row_height + measure_row_height / 2 - partial_data_indicator_size / 2 %>"
dominant-baseline="middle" >
<i class="fas fa-exclamation-circle"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-content="The following sources are not included in this measure due to insufficient data: <%= presenter.partial_data_sources.join(' and ') %>." ></i>
</foreignObject>
<% end %>
<% end %>
</svg>
<svg id="measure-row-labels" x="0" y=<%= heading_gutter %>>
<% displayed_presenters.each_with_index do |presenter, index| %>
<foreignObject
x="<%= availability_indicator_percentage %>%"
y="<%= index * measure_row_height + measure_row_height / 4 %>"
dominant-baseline="middle"
data-variance-row-label
width="550"
height="200">
<%= link_to(presenter.measure_name, district_school_category_path( @district, @school, presenter.category, {year: @academic_year.range, anchor: "#{presenter.measure_id}"}), class: "measure-row-label") %>
</foreignObject>
<% end %>
<% if displayed_presenters.none? %>
<text
class="font-cabin"
x="0"
y="<%= 0 * measure_row_height + measure_row_height / 2 %>"
dominant-baseline="middle"
data-variance-row-label
>
Insufficient data
</text>
<% end %>
</svg>
<svg
id="measure-row-bars"
x="<%= label_width_percentage %>%"
y="<%= heading_gutter %>"
width="<%= graph_width_percentage %>%"
>
<% displayed_presenters.each_with_index do |presenter, index| %>
<rect
class="measure-row-bar <%= presenter.bar_color %>"
x="<%= presenter.x_offset %>"
y="<%= index * measure_row_height + (measure_row_height - measure_row_bar_height) / 2 %>"
width="<%= presenter.bar_width %>"
height="<%= measure_row_bar_height %>"
data-for-measure-id="<%= presenter.measure_id %>"
stroke="none"
/>
<% end %>
</svg>
</g>
<svg
id="key-benchmark"
x="<%= label_width_percentage %>%"
y="0"
width="<%= graph_width_percentage %>%"
height="<%= graph_background_height(number_of_rows: displayed_presenters.size) %>"
>
<g transform="translate(0, <%= heading_gutter %>)">
<rect
id="key-benchmark"
x="60%"
transform="translate(-1, 0)"
y="0"
width="4"
height="100%"
fill="black"
/>
</g>
</svg>
<svg
id="legend"
x="<%= label_width_percentage %>%"
y="0"
width="<%= graph_width_percentage %>%"
>
<text
class="graph-footer"
x="60%"
y="<%= graph_background_height(number_of_rows: displayed_presenters.size) + (footer_gutter / 2) %>"
text-anchor="middle"
>
Benchmark
</text>
</svg>
</svg>

@ -0,0 +1,100 @@
<% content_for :navigation do %>
<h2 class="sub-header-2 color-white m-0">Areas Of Interest</h2>
<select id="select-academic-year" class="form-select" name="academic-year">
<% @academic_years.each do |year| %>
<option value="<%= district_school_overview_index_path(@district, @school, {year: year.range}) %>" <%= @academic_year == year ? "selected" : nil %>><%= year.formatted_range %></option>
<% end %>
</select>
<% end %>
<% cache do %>
<svg class="d-none">
<symbol viewBox="0 0 24 24" id="warning-harvey-ball">
<circle cx="12" cy="12" r="11.5" fill="white" stroke="none" />
<path d="
M 12 0
A 12 12 0 0 1 24 12
L 12 12
L 12 0"
stroke="none"
/>
<circle cx="12" cy="12" r="11.5" fill="none" />
</symbol>
<symbol viewBox="0 0 24 24" id="watch-harvey-ball">
<circle cx="12" cy="12" r="11.5" fill="white" stroke="none" />
<path d="
M 12 0
A 12 12 0 1 1 12 24
L 12 12
L 12 0"
stroke="none"
/>
<circle cx="12" cy="12" r="11.5" fill="none" />
</symbol>
<symbol viewBox="0 0 24 24" id="growth-harvey-ball">
<circle cx="12" cy="12" r="11.5" fill="white" stroke="none" />
<path d="
M 12 0
A 12 12 0 1 1 0 12
L 12 12
L 12 0"
stroke="none"
/>
<circle cx="12" cy="12" r="11.5" fill="none" />
</symbol>
<symbol viewBox="0 0 24 24" id="approval-harvey-ball">
<circle cx="12" cy="12" r="11.5" />
<path d="M19 8C19 8.28125 18.875 8.53125 18.6875 8.71875L10.6875 16.7188C10.5 16.9062 10.25 17 10 17C9.71875 17 9.46875 16.9062 9.28125 16.7188L5.28125 12.7188C5.09375 12.5312 5 12.2812 5 12C5 11.4375 5.4375 11 6 11C6.25 11 6.5 11.125 6.6875 11.3125L10 14.5938L17.2812 7.3125C17.4688 7.125 17.7188 7 18 7C18.5312 7 19 7.4375 19 8Z"
stroke-width=".5" stroke="white" fill="white" />
</symbol>
<symbol viewBox="0 0 24 24" id="ideal-harvey-ball">
<circle cx="12" cy="12" r="11.5" />
<path d="M9.28125 11.7188C9.46875 11.9062 9.71875 12 10 12C10.25 12 10.5 11.9062 10.6875 11.7188L15.6875 6.71875C15.875 6.53125 16 6.28125 16 6C16 5.4375 15.5312 5 15 5C14.7188 5 14.4688 5.125 14.2812 5.3125L10 9.59375L8.1875 7.8125C8 7.625 7.75 7.5 7.5 7.5C6.9375 7.5 6.5 7.9375 6.5 8.5C6.5 8.78125 6.59375 9.03125 6.78125 9.21875L9.28125 11.7188ZM19 10C19 9.4375 18.5312 9 18 9C17.7188 9 17.4688 9.125 17.2812 9.3125L10 16.5938L6.6875 13.3125C6.5 13.125 6.25 13 6 13C5.4375 13 5 13.4375 5 14C5 14.2812 5.09375 14.5312 5.28125 14.7188L9.28125 18.7188C9.46875 18.9062 9.71875 19 10 19C10.25 19 10.5 18.9062 10.6875 18.7188L18.6875 10.7188C18.875 10.5312 19 10.2812 19 10Z"
stroke-width=".5" stroke="white" fill="white" />
</symbol>
<symbol viewBox="0 0 24 24" id="insufficient_data-harvey-ball">
<circle cx="12" cy="12" r="11.5" />
</symbol>
</svg>
<% end %>
<% cache [@school, @academic_year] do %>
<div class="card">
<div class="d-flex justify-content-between align-items-center">
<h2 class="sub-header-2">School Quality Framework Indicators</h2>
<div class="harvey-ball-legend">
<div class="font-size-14">Warning</div>
<svg class="ms-3 me-1" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<use class="harvey-ball harvey-ball--warning" xlink:href="#warning-harvey-ball"></use>
</svg>
<svg class="mx-1" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<use class="harvey-ball harvey-ball--watch" xlink:href="#watch-harvey-ball"></use>
</svg>
<svg class="mx-1" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<use class="harvey-ball harvey-ball--growth" xlink:href="#growth-harvey-ball"></use>
</svg>
<svg class="mx-1" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<use class="harvey-ball harvey-ball--approval" xlink:href="#approval-harvey-ball"></use>
</svg>
<svg class="ms-1 me-3" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<use class="harvey-ball harvey-ball--ideal" xlink:href="#ideal-harvey-ball"></use>
</svg>
<div class="font-size-14">Ideal</div>
</div>
</div>
<%= render partial: "quality_framework_indicators", locals: { category_presenters: @category_presenters } %>
<div class="overall-response-rate-row">
<%= render partial: "response_rate", locals: {response_rate_presenter: @student_response_rate_presenter} %>
<%= render partial: "response_rate", locals: {response_rate_presenter: @teacher_response_rate_presenter} %>
</div>
</div>
<div class="card">
<h2 class="sub-header-2 mb-4">Distance From Benchmark</h2>
<%= render partial: "variance_chart", locals: { presenters: @variance_chart_row_presenters } %>
</div>
<% if @district == District.find_by_name("Boston") %>
<%= render partial: 'layouts/boston_modal' %>
<% elsif @has_empty_dataset %>
<%= render partial: 'layouts/empty_dataset_modal' %>
<% end %>
<% end %>

@ -0,0 +1,13 @@
# frozen_string_literal: true
module ArrayMonkeyPatches
def average
sum.to_f / size
end
def remove_blanks
reject { |item| item.nil? || item.to_f.nan? || item.zero? }
end
end
Array.include ArrayMonkeyPatches

@ -0,0 +1,21 @@
# frozen_string_literal: true
module FloatMonkeyPatches
def round_up_to_one
if positive? && self < 1
1.0
else
self
end
end
def round_down_to_five
if positive? && self > 5
5.0
else
self
end
end
end
Float.include FloatMonkeyPatches

@ -1,3 +1,10 @@
Dashboard::Engine.routes.draw do
resources :districts do
resources :schools, only: %i[index show] do
resources :overview, only: [:index]
# resources :categories, only: [:show], path: "browse"
# resources :analyze, only: [:index]
end
end
get "/welcome", to: "home#index"
end

@ -4,7 +4,7 @@ class CreateDashboardResponseRates < ActiveRecord::Migration[7.1]
t.references :dashboard_subcategory, null: false, foreign_key: true
t.references :dashboard_school, null: false, foreign_key: true
t.references :dashboard_academic_year, null: false, foreign_key: true
t.float :school_response_rate
t.float :student_response_rate
t.float :teacher_response_rate
t.timestamps

@ -134,7 +134,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_04_192128) do
t.bigint "dashboard_subcategory_id", null: false
t.bigint "dashboard_school_id", null: false
t.bigint "dashboard_academic_year_id", null: false
t.float "school_response_rate"
t.float "student_response_rate"
t.float "teacher_response_rate"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false

Loading…
Cancel
Save