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
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in new issue