OpenCare
Dashboard
  • Overview
  • Grade comparison
  • Grade history
import { aq, op } from '@uwdata/arquero'
import { Plot } from "@observablehq/plot"

// Import time parser
d3 = require("d3@7")
parser = d3.utcParse("%Y-%m-%d");

// Transpose data to make it useable in ojs inputs
datastore_transposed = transpose(datastore)
viewof search = Inputs.search(datastore_transposed, {placeholder: "Type to find a service",
width: 150,
height: 10})

// Create drop down menu of service names
viewof items = Inputs.table(search, {
columns: ["service"],
header: {service: "Select:"},
width: 300,
height: 250,
required: true,
value: search, 
multiple: false})
function return_name() {
  var name = "Select a service"
  if(items === null) {
    return name
  } else {
    return items.service
  }
}

service_selected_name = return_name()

// Create function to output row of selected service
function filtered_df() {
  var name = "Select a service"
  if(items === null) {
    return name
  } else {
    return items
  }
}

service_selected = filtered_df()
Disclaimer

This dashboard is not affiliated with or supported by the Care Inspectorate. It contains public sector information licensed under the Open Government Licence v3.0.

Data source
Last refreshed: 14 Aug 2025 10:30:44
Data as at: 30 Jun 2025
mytable = aq.table(datastore)

// Create filtered table for same service type for comparison calculations
selected_service_compare = mytable
  .params({
  // define parameters
    selected_CareService: service_selected.CareService,
    selected_Subtype: service_selected.Subtype
  })
  // filter to only include services of the same type
  .filter((d, $) => op.includes($.selected_CareService, d.CareService) &&
    (
      $.selected_Subtype == null || $.selected_Subtype == "NA" || d.Subtype == null || d.Subtype == "NA"
        ? true // If either selected or record Subtype is NA/null, include it
        : op.includes($.selected_Subtype, d.Subtype)
    ))

Service type

service_name_check = {
  const subtype = service_selected?.Subtype;
  if (subtype === undefined || subtype === null || subtype === "NA") {
   return service_selected.CareService;
    } else {
    return (service_selected.CareService + " - " + service_selected.Subtype);
  }
}


// SELECTED SERVICE TYPE
selected_serv_type  = {
  if (service_selected.CareService) {
    return service_name_check;
  } else {
    return "Select a service";
  }
}

number_services_same_type = selected_service_compare
  .rollup({
      count: op.count()
  })
   .objects()[0].count
   
// NUMBERS OF SERVICES WITH NO GRADE
number_services_same_type_no_grade = selected_service_compare
 .filter(d => op.includes("No grade yet", d.MinGrade))
  .rollup({
      count: op.count()
  })
   .objects()[0].count
   
// TEXT STRING SUBTITLE
selected_serv_type_subtitle  = {
  if (service_selected.CareService) {
    return ( "There are " + number_services_same_type + " of these services, " + number_services_same_type_no_grade + " have not yet received grades" );
  } else {
    return "";
  }
}

Lowest grade

// SELECTED SERVICE TYPE
selected_serv_grade  = {
  if (service_selected.CareService) {
    return (service_selected.MinGrade);
  } else {
    return "Select a service";
  }
}

perc_services_same_grade = selected_service_compare
.params({
  // define parameters
    selected_MinGrade: service_selected.MinGrade
  })
  // first filter out those that don't have any grades
  .filter(d  => d.MinGrade_numeric >= 1)
  // group by each grade
  .groupby("MinGrade")
  // calculate counts of services with each mingrade
  .rollup({
      count: op.count()
  })
  // create new column for proportion of services with each grade
  .derive({ percent: d => op.round((d.count / op.sum(d.count) || 0)*100) }) // include `|| 0` in case sum is zero
  // filter out match selected grade
  .filter((d, $) => op.includes($.selected_MinGrade, d.MinGrade))
  // pull out percent value
   .objects()[0].percent
  
   
// PERCENTAGE OF SERVICES WITH HIGHER GRADE
perc_services_higher_grade = selected_service_compare
.params({
  // define parameters
     selected_MinGrade_numeric: service_selected.MinGrade_numeric
  })
  // first filter out those that don't have any grades
  .filter(d  => d.MinGrade_numeric >= 1)
  // group by each grade
  .groupby("MinGrade_numeric")
  // calculate counts of services with each mingrade
  .rollup({
      count: op.count()
  })
  // create new column for proportion of services with each grade
  .derive({ percent: d => op.round((d.count / op.sum(d.count) || 0)*100) }) // include `|| 0` in case sum is zero
  // Ungroup by minimum grade
  .ungroup()
  // filter out match selected grade
  .filter((d, $) => d.MinGrade_numeric > $.selected_MinGrade_numeric)
  .derive({total: d => op.sum(d.percent)})
  // pull out total value
  .object(1)?.total ?? 0
   
// TEXT STRING SUBTITLE
selected_serv_grade_subtitle  = {
  if (service_selected.CareService) {
    return ( "Same as " + perc_services_same_grade + "% of services of the same type, whilst " + perc_services_higher_grade + "% have a higher minimum grade" );
  } else {
    return "";
  }
}

Last inspected

inspection_check = {
  if(service_selected.months_since_inspection >=0) {
    return (service_selected.months_since_inspection + " month(s) since last inspection");
  } else {
    return "No inspection yet";
  }
}

// SELECTED SERVICE LAST INSPECTION
selected_serv_last_inspection  = {
  if (service_selected.CareService) {
    return inspection_check; 
  } else {
    return "Select a service";
  }
}

median_time_last_inspection = selected_service_compare 
  // filter out those that haven't yet been inspected
 .filter(d => d.months_since_inspection >= 0)
  .rollup({
    count: op.count(),
    median_months_since_inspection: op.median('months_since_inspection')
   })
   .objects()[0].median_months_since_inspection
   
// TEXT STRING SUBTITLE
selected_serv_last_inspection_subtitle  = {
  if (service_selected.CareService) {
    return "The average (median) time since the last inspection for services of this type is " + median_time_last_inspection + " month(s)";
  } else{
    return "";
  }
}

Number of staff

staff_check = {
  if(service_selected.staff_per_place >=0) {
    return (service_selected.staff_per_place.toFixed(2) + " staff per registered place");
  } else {
    return "No staff recorded";
  }
}

// SELECTED SERVICE LAST INSPECTION
staff_per_place  = {
  if (service_selected.CareService) {
    return staff_check; 
  } else {
    return "Select a service";
  }
}

median_staff = selected_service_compare 
 .filter(d => d.staff_per_place > 0)
  .rollup({
    count: op.count(),
    median_staff_per_place: op.median('staff_per_place')
   })
  .objects()[0]?.median_staff_per_place ?? "No staff recorded"
  
  
// First check for those that are undefined as all services have had no complaints
staff_undefined_check = {
  if(median_staff !== "No staff recorded"){
    return "The average (median) number of staff per registered place for services of this type is " + median_staff.toFixed(2);
  } else {
    return "No services of this type had any staff recorded";
  }
}
  
// TEXT STRING SUBTITLE
median_staff_subtitle  = {
  if (service_selected.CareService) {
    return staff_undefined_check;
  } else{
    return "";
  }
}

Upheld complaints

complaints_check = {
  if(service_selected.Complaints_upheld_2425 >=0) {
    return (service_selected.Complaints_upheld_2425 + " upheld complaint(s) between 1 April 2024 and 31 March 2025");
  } else {
    return "No upheld complaints between 1 April 2024 and 31 March 2025";
  }
}

// SELECTED SERVICE COMPLAINTS
complaints  = {
  if (service_selected.CareService) {
    return complaints_check ; 
  } else {
    return "Select a service";
  }
}

perc_services_complaint = selected_service_compare
  // group by each grade
  .groupby("Complaints_upheld_2425")
  // calculate counts of services with all the varying numbers of upheld complaints
  .rollup({
      count: op.count()
  })
  // create new column for proportion of services with each number of complaints
  .derive({ percent: d => op.round((d.count / op.sum(d.count) || 0)*100) }) // include `|| 0` in case sum is zero
  // Ungroup by number of complaints
  .ungroup()
  // filter out anything less than 1
  .filter((d) => d.Complaints_upheld_2425 >= 1)
  .derive({total: d => op.sum(d.percent)})
  // pull out total value
  .objects()[0]?.total ?? 0

// First check for those that are undefined as all services have had no complaints
complaints_undefined_check = {
  if(perc_services_complaint !== 0){
    return perc_services_complaint + "% of services of this type had at least one upheld complaint between 1 April 2024 and 31 March 2025";
  } else {
    return "No services of this type had at least one upheld complaint between 1 April 2024 and 31 March 2025";
  }
}
  
// TEXT STRING SUBTITLE
complaints_subtitle  = {
  if (service_selected.CareService) {
    return complaints_undefined_check;
  } else{
    return "";
  }
}

Requirements from inspection

requirements_check = {
  if(service_selected.any_requirements_2425 === "Yes") {
    return ("This service had requirement(s) from inspections between 1 April 2024 and 31 March 2025");
  } else  if(service_selected.any_requirements_2425 === "Inspected but no requirements") {
    return "This service had no requirement(s) from inspections between 1 April 2024 and 31 March 2025";
  } else {
  return "This service was not inspected between 1 April 2024 and 31 March 2025"
  }
}

// SELECTED SERVICE REQUIREMENTS
requirements  = {
  if (service_selected.CareService) {
    return requirements_check ; 
  } else {
    return "Select a service";
  }
}

perc_services_requirement = selected_service_compare
  // First filter out those that have not been inspected (i.e. blank field)
  .filter((d) => d.any_requirements_2425 !== "")
  // group by each grade
  .groupby("any_requirements_2425")
  // calculate counts of services with all the varying categories of requirements (yes, inspected but none, and blank)
  .rollup({
      count: op.count()
  })
  // create new column for proportion of services with each number of requirements
  .derive({ percent: d => op.round((d.count / op.sum(d.count) || 0)*100) }) // include `|| 0` in case sum is zero
  // Ungroup by number of requirements
  .ungroup()
  // filter only those that are "Yes"
  .filter((d) => d.any_requirements_2425 === "Yes")
  .derive({total: d => op.sum(d.percent)})
  // pull out total value
  .objects()[0]?.total ?? 0

// First check for those that are undefined as all services have had no complaints
requirements_undefined_check = {
  if(perc_services_requirement !== 0){
    return perc_services_requirement + "% of services of this type that had been inspected between 1 April 2024 and 31 March 2025 had at least one requirement";
  } else {
    return "No services of this type that had been inspected between 1 April 2024 and 31 March 2025 had any requirements";
  }
}
  
// TEXT STRING SUBTITLE
requirements_subtitle  = {
  if (service_selected.CareService) {
    return requirements_undefined_check;
  } else{
    return "";
  }
}
grades = ["Unsatisfactory", "Weak", "Adequate", "Good", "Very good", "Excellent", "No grade yet"]

// --- 1. Selected Service Plot Data ---
selected_wrangled = aq.from([service_selected]) // Wrap in array because service_selected is one row
  .fold(Object.keys(service_selected).filter(d => d.startsWith("KQ")), { as: ["kq", "grade"] })
  .groupby("kq", "grade")
  .rollup({ count: op.count() })
  .ungroup("grade")
  // Mapping KQ codes to readable labels
  .derive({
    kq_label: (d) => op.recode(d.kq, { 'KQ_Support_Wellbeing': 'How well do we support people’s wellbeing?','KQ_Staff_Team': 'How good is our staff team?', 'KQ_Setting': 'How good is our setting?', 'KQ_Leadership': 'How good is our leadership?', 'KQ_Care_Play_and_Learning': 'How good is our care, play and learning?', 'KQ_Care_and_Support_Planning': 'How well is our care and support planned?'}, '?')
  })
  
// --- 2. Comparison Group Data ---
comparison_wrangled = selected_service_compare
.params({
  // define parameters
    selected_KQ_Care_and_Support_Planning: service_selected.KQ_Care_and_Support_Planning,
    selected_KQ_Support_Wellbeing: service_selected.KQ_Support_Wellbeing,
    selected_KQ_Staff_Team: service_selected.KQ_Staff_Team,
    selected_KQ_Setting: service_selected.KQ_Setting,
    selected_KQ_Leadership: service_selected.KQ_Leadership,
    selected_KQ_Care_Play_and_Learning: service_selected.KQ_Care_Play_and_Learning
    
  })
  .fold(Object.keys(service_selected).filter(d => d.startsWith("KQ")), { as: ["kq", "grade"] })
  .groupby("kq", "grade")
  .rollup({ count: op.count() })
  .filter((d) => d.grade !== "No grade yet")
  .ungroup()
  // Mapping KQ codes to readable labels
  .derive({
    kq_label: (d) => op.recode(d.kq, { 'KQ_Support_Wellbeing': 'How well do we support people’s wellbeing?','KQ_Staff_Team': 'How good is our staff team?', 'KQ_Setting': 'How good is our setting?', 'KQ_Leadership': 'How good is our leadership?', 'KQ_Care_Play_and_Learning': 'How good is our care, play and learning?', 'KQ_Care_and_Support_Planning': 'How well is our care and support planned?'}, '?')
  })
  // Create numbered equivalent of grades for orderby later
  .derive({
  grade_value: (d) => op.recode(d.grade, { 'Unsatisfactory': 1,'Weak': 2, 'Adequate': 3, 'Good': 4, 'Very good': 5, 'Excellent': 6}, '?')
})
  .groupby("kq")
  .derive({
    proportion: d => d.count / op.sum(d.count)
  })
  .orderby("kq_label", "grade_value")
  .derive({
  // Calculate cumsum
    cumsum: aq.rolling(d => op.sum(d.proportion))
  })
    // calculate proportion of services GREATER THAN so it's 1 minus the cumsum
  .derive({
    oneminus_cumsum: d => 1 - d.cumsum
})
  .derive({
     comparison_text: d => "Same as " + (op.round(d.proportion*100)) + "% of services of the same type, whilst " + (op.round(d.oneminus_cumsum*100)) + "% have a higher grade"
  })
  .filter((d, $) =>
    (d.kq === "KQ_Care_and_Support_Planning" && d.grade ===  $.selected_KQ_Care_and_Support_Planning) ||
    (d.kq === "KQ_Support_Wellbeing" && d.grade ===  $.selected_KQ_Support_Wellbeing) ||
    (d.kq === "KQ_Staff_Team" && d.grade ===  $.selected_KQ_Staff_Team) ||
    (d.kq === "KQ_Setting" && d.grade ===  $.selected_KQ_Setting) ||
    (d.kq === "KQ_Leadership" && d.grade ===  $.selected_KQ_Leadership) ||
    (d.kq === "KQ_Care_Play_and_Learning" && d.grade ===  $.selected_KQ_Care_Play_and_Learning)
  )
How good is our care, play and learning?

How good is our leadership?

How good is our setting?

How good is our staff team?

How well do we support people’s wellbeing?

How well is our care and support planned?

// centred plot with minimal whitespace
Plot.plot({
  x: { 
    domain: [0, 1],
    axis: null,
    label: null,
    tickSize: 0 
    }, // removes x-axis
  y: {
    axis: null,
    tickSize: 0,
    label: null
    }, // removes y-axis
  marks: [
    // Big colored dots
    Plot.dot(selected_wrangled.objects().filter(d => d.kq === "KQ_Care_Play_and_Learning"), {
      y: "kq_label",
      x: () => 0.5,
      fill: "grade",
      r: 42,
      inset: 0 // remove extra padding around the dot
    }),
    // Grade label inside dot
    Plot.text(selected_wrangled.objects().filter(d => d.kq === "KQ_Care_Play_and_Learning"), {
      y: "kq_label",
      x: () => 0.5,
      text: "grade",
      fill: "white",
      fontWeight: "bold",
      fontSize: 12
    })],
  x: {
    domain: [0, 1],
    axis: null,
    label: null,
    tickFormat: ""
  },
  color: {
    domain: grades,
    range: ["#d73027", "#fc8d59", "#fdcc3f", "#c3e648", "#91cf60", "#1a9850", "grey"],
    legend: false
  },
  style: {
    fontSize: "20px",  
    fontWeight: "bold",
    labelAnchor: "left"
  },
  width: 85,
  height: 85,
  margin: 0 // removes all outer whitespace
})
// centred plot with minimal whitespace
Plot.plot({
  x: { 
    domain: [0, 1],
    axis: null,
    label: null,
    tickSize: 0 
    }, // removes x-axis
  y: {
    axis: null,
    tickSize: 0,
    label: null
    }, // removes y-axis
  marks: [
    // Big colored dots
    Plot.dot(selected_wrangled.objects().filter(d => d.kq === "KQ_Leadership"), {
      y: "kq_label",
      x: () => 0.5,
      fill: "grade",
      r: 42,
      inset: 0 // remove extra padding around the dot
    }),
    // Grade label inside dot
    Plot.text(selected_wrangled.objects().filter(d => d.kq === "KQ_Leadership"), {
      y: "kq_label",
      x: () => 0.5,
      text: "grade",
      fill: "white",
      fontWeight: "bold",
      fontSize: 12
    })],
  x: {
    domain: [0, 1],
    axis: null,
    label: null,
    tickFormat: ""
  },
  color: {
    domain: grades,
    range: ["#d73027", "#fc8d59", "#fdcc3f", "#c3e648", "#91cf60", "#1a9850", "grey"],
    legend: false
  },
  style: {
    fontSize: "20px",  
    fontWeight: "bold",
    labelAnchor: "left"
  },
  width: 85,
  height: 85,
  margin: 0 // removes all outer whitespace
})
// centred plot with minimal whitespace
Plot.plot({
  x: { 
    domain: [0, 1],
    axis: null,
    label: null,
    tickSize: 0 
    }, // removes x-axis
  y: {
    axis: null,
    tickSize: 0,
    label: null
    }, // removes y-axis
  marks: [
    // Big colored dots
    Plot.dot(selected_wrangled.objects().filter(d => d.kq === "KQ_Setting"), {
      y: "kq_label",
      x: () => 0.5,
      fill: "grade",
      r: 42,
      inset: 0 // remove extra padding around the dot
    }),
    // Grade label inside dot
    Plot.text(selected_wrangled.objects().filter(d => d.kq === "KQ_Setting"), {
      y: "kq_label",
      x: () => 0.5,
      text: "grade",
      fill: "white",
      fontWeight: "bold",
      fontSize: 12
    })],
  x: {
    domain: [0, 1],
    axis: null,
    label: null,
    tickFormat: ""
  },
  color: {
    domain: grades,
    range: ["#d73027", "#fc8d59", "#fdcc3f", "#c3e648", "#91cf60", "#1a9850", "grey"],
    legend: false
  },
  style: {
    fontSize: "20px",  
    fontWeight: "bold",
    labelAnchor: "left"
  },
  width: 85,
  height: 85,
  margin: 0 // removes all outer whitespace
})
// centred plot with minimal whitespace
Plot.plot({
  x: { 
    domain: [0, 1],
    axis: null,
    label: null,
    tickSize: 0 
    }, // removes x-axis
  y: {
    axis: null,
    tickSize: 0,
    label: null
    }, // removes y-axis
  marks: [
    // Big colored dots
    Plot.dot(selected_wrangled.objects().filter(d => d.kq === "KQ_Staff_Team"), {
      y: "kq_label",
      x: () => 0.5,
      fill: "grade",
      r: 42,
      inset: 0 // remove extra padding around the dot
    }),
    // Grade label inside dot
    Plot.text(selected_wrangled.objects().filter(d => d.kq === "KQ_Staff_Team"), {
      y: "kq_label",
      x: () => 0.5,
      text: "grade",
      fill: "white",
      fontWeight: "bold",
      fontSize: 12
    })],
  x: {
    domain: [0, 1],
    axis: null,
    label: null,
    tickFormat: ""
  },
  color: {
    domain: grades,
    range: ["#d73027", "#fc8d59", "#fdcc3f", "#c3e648", "#91cf60", "#1a9850", "grey"],
    legend: false
  },
  style: {
    fontSize: "20px",  
    fontWeight: "bold",
    labelAnchor: "left"
  },
  width: 85,
  height: 85,
  margin: 0 // removes all outer whitespace
})
// centred plot with minimal whitespace
Plot.plot({
  x: { 
    domain: [0, 1],
    axis: null,
    label: null,
    tickSize: 0 
    }, // removes x-axis
  y: {
    axis: null,
    tickSize: 0,
    label: null
    }, // removes y-axis
  marks: [
    // Big colored dots
    Plot.dot(selected_wrangled.objects().filter(d => d.kq === "KQ_Support_Wellbeing"), {
      y: "kq_label",
      x: () => 0.5,
      fill: "grade",
      r: 42,
      inset: 0 // remove extra padding around the dot
    }),
    // Grade label inside dot
    Plot.text(selected_wrangled.objects().filter(d => d.kq === "KQ_Support_Wellbeing"), {
      y: "kq_label",
      x: () => 0.5,
      text: "grade",
      fill: "white",
      fontWeight: "bold",
      fontSize: 12
    })],
  x: {
    domain: [0, 1],
    axis: null,
    label: null,
    tickFormat: ""
  },
  color: {
    domain: grades,
    range: ["#d73027", "#fc8d59", "#fdcc3f", "#c3e648", "#91cf60", "#1a9850", "grey"],
    legend: false
  },
  style: {
    fontSize: "20px",  
    fontWeight: "bold",
    labelAnchor: "left"
  },
  width: 85,
  height: 85,
  margin: 0 // removes all outer whitespace
})
// centred plot with minimal whitespace
Plot.plot({
  x: { 
    domain: [0, 1],
    axis: null,
    label: null,
    tickSize: 0 
    }, // removes x-axis
  y: {
    axis: null,
    tickSize: 0,
    label: null
    }, // removes y-axis
  marks: [
    // Big colored dots
    Plot.dot(selected_wrangled.objects().filter(d => d.kq === "KQ_Care_and_Support_Planning"), {
      y: "kq_label",
      x: () => 0.5,
      fill: "grade",
      r: 42,
      inset: 0 // remove extra padding around the dot
    }),
    // Grade label inside dot
    Plot.text(selected_wrangled.objects().filter(d => d.kq === "KQ_Care_and_Support_Planning"), {
      y: "kq_label",
      x: () => 0.5,
      text: "grade",
      fill: "white",
      fontWeight: "bold",
      fontSize: 12
    })],
  x: {
    domain: [0, 1],
    axis: null,
    label: null,
    tickFormat: ""
  },
  color: {
    domain: grades,
    range: ["#d73027", "#fc8d59", "#fdcc3f", "#c3e648", "#91cf60", "#1a9850", "grey"],
    legend: false
  },
  style: {
    fontSize: "20px",  
    fontWeight: "bold",
    labelAnchor: "left"
  },
  width: 85,
  height: 85,
  margin: 0 // removes all outer whitespace
})
inspection_history = aq.table(inspection_series)

// Create filtered table for selected service inspection history
inspection_history_table = inspection_history
  .params({
  // define parameters
    selected_CSNumber: service_selected.CSNumber,
  })
  // filter to only include services of the same type
  .filter((d, $) => d.CSNumber === $.selected_CSNumber)
selected_likert_table = aq.from(inspection_history_table)
  .fold(inspection_history_table.columnNames().slice(4, 17), {
    as: ['Question', 'Grade']
  })
  .derive({
  Grade_descr: (d) => op.recode(d.Grade, { '1':'Unsatisfactory', '2':'Weak', '3':'Adequate', '4': 'Good', '5': 'Very good',  '6': 'Excellent'}, '?')
  })
  .filter(d => d.Grade != null)
  .derive({
    Question: d => op.replace(d.Question, 'Quality_of_', ' ')
  })
  .derive({
    Question: d => op.replace(d.Question, 'KQ_', ' ')
  })
  .derive({
    Question: d => op.replace(d.Question, /_/g, ' ')
  })
  .groupby("CSNumber", "ServiceName", "Publication_of_Latest_Grading", "Last_inspection_Date", "URL","Grade", "Grade_descr")
  .pivot("Question", "Question")
  .derive({
  // Dynamic column selection not permitted so concatenate all rows then filter after
  Themes: d => op.compact(op.values(op.row_object()))
  })
   .derive({
   // replace the first 7 columns worth of data so only left with concatenated themes
    Themes: d => op.replace(d.Themes, /(.+?,){7}/, ' '),
  })
  .ungroup()
  // Now need to pivot longer to get back in the right format
  .fold(aq.not('CSNumber', 'ServiceName', 'Publication_of_Latest_Grading', 'Last_inspection_Date', 'URL', 'Grade', 'Grade_descr', 'Themes'))
  .filter(d => d.value != null)
  .groupby('Last_inspection_Date', 'Grade_descr')
  .derive({
    count: aq.op.count()
  })
  .ungroup()
  .dedupe(['Last_inspection_Date', 'Grade_descr'])
  .derive({ date_parsed: aq.escape(d => parser(d.Last_inspection_Date)) })
  
// --- 2. Calculate proportions of each grade ---
calc_selected_likert_table = selected_likert_table
  .groupby('Last_inspection_Date')
  .derive({
    total: aq.op.sum('count')
  })
  .derive({
    proportion: d => d.count / d.total
  })
  .objects()
Plot.plot({
marginBottom: 80,
x: {
tickFormat: "",
interval: "month",
type: "band",
label: "Date of inspection"
},
  marks: [
    Plot.barY(calc_selected_likert_table, {
      x: "date_parsed",
      y: "proportion",
      fill: "Grade_descr",
      order: "Grade",
      channels: {
      'Proportion of all grades': b => `${(b.proportion*100).toFixed(0)}%`,
      'Date of inspection': "date_parsed", 
      'Date grades published': "Publication_of_Latest_Grading",
      'Themes': "Themes"},
      tip: true
    }),
    Plot.axisY({ tickFormat: d => `${Math.round(d)}%`, label: ""}),
    Plot.axisX({ tickRotate: 0, label: "Inspections" })
  ],
  y: {
  grid: true,
  label: "Proportion of all grades",
  percent: true
},
  color: {
  label: "Grade awarded",
    domain: ["Unsatisfactory", "Weak", "Adequate", "Good", "Very good", "Excellent"],
    range: ["#d73027", "#fc8d59", "#fdcc3f", "#c3e648", "#91cf60", "#1a9850"],
    legend: true,
    legendPosition: "top"
  },
  style: {
  fontSize: "14px"
},
  width: 1000,
  height: 400
})