/** * The contents of this file are subject to the OpenMRS Public License * Version 1.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * http://license.openmrs.org * * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the * License for the specific language governing rights and limitations * under the License. * * Copyright (C) OpenMRS, LLC. All Rights Reserved. */ package org.openmrs.web.controller; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Vector; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jfree.chart.ChartFactory; import org.jfree.chart.JFreeChart; import org.jfree.chart.plot.PlotOrientation; import org.jfree.data.general.DefaultPieDataset; import org.jfree.data.statistics.HistogramDataset; import org.jfree.data.time.Day; import org.jfree.data.time.TimeSeries; import org.jfree.data.time.TimeSeriesCollection; import org.openmrs.Concept; import org.openmrs.ConceptDatatype; import org.openmrs.Obs; import org.openmrs.api.ConceptService; import org.openmrs.api.ObsService; import org.openmrs.api.context.Context; import org.openmrs.util.OpenmrsConstants; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.validation.BindException; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.SimpleFormController; import org.springframework.web.servlet.view.RedirectView; public class ConceptStatsFormController extends SimpleFormController { /** Logger for this class and subclasses */ protected final Log log = LogFactory.getLog(getClass()); /** * @see org.springframework.web.servlet.mvc.AbstractFormController#processFormSubmission(javax.servlet.http.HttpServletRequest, * javax.servlet.http.HttpServletResponse, java.lang.Object, * org.springframework.validation.BindException) */ protected ModelAndView processFormSubmission(HttpServletRequest request, HttpServletResponse response, Object object, BindException errors) throws Exception { Concept concept = (Concept) object; ConceptService cs = Context.getConceptService(); // check to see if they clicked next/previous concept: String jumpAction = request.getParameter("jumpAction"); if (jumpAction != null) { Concept newConcept = null; if ("previous".equals(jumpAction)) newConcept = cs.getPrevConcept(concept); else if ("next".equals(jumpAction)) newConcept = cs.getNextConcept(concept); if (newConcept != null) return new ModelAndView(new RedirectView(getSuccessView() + "?conceptId=" + newConcept.getConceptId())); } return new ModelAndView(new RedirectView(getSuccessView())); } /** * This is called prior to displaying a form for the first time. It tells Spring the * form/command object to load into the request * * @see org.springframework.web.servlet.mvc.AbstractFormController#formBackingObject(javax.servlet.http.HttpServletRequest) */ protected Object formBackingObject(HttpServletRequest request) throws ServletException { Concept concept = null; ConceptService cs = Context.getConceptService(); String conceptId = request.getParameter("conceptId"); if (conceptId != null) { concept = cs.getConcept(Integer.valueOf(conceptId)); } if (concept == null) concept = new Concept(); return concept; } /** * Called prior to form display. Allows for data to be put in the request to be used in the view * * @see org.springframework.web.servlet.mvc.SimpleFormController#referenceData(javax.servlet.http.HttpServletRequest) */ protected Map<String, Object> referenceData(HttpServletRequest request) throws Exception { Map<String, Object> map = new HashMap<String, Object>(); if (!Context.hasPrivilege("View Observations")) return map; MessageSourceAccessor msa = getMessageSourceAccessor(); Locale locale = Context.getLocale(); ConceptService cs = Context.getConceptService(); String conceptId = request.getParameter("conceptId"); //List<Obs> obs = new Vector<Obs>(); //List<Obs> obsAnswered = new Vector<Obs>(); if (conceptId != null) { Concept concept = cs.getConcept(Integer.valueOf(conceptId)); ObsService obsService = Context.getObsService(); if (concept != null) { // previous/next ids for links map.put("previousConcept", cs.getPrevConcept(concept)); map.put("nextConcept", cs.getNextConcept(concept)); //obs = obsService.getObservations(concept, "valueNumeric, obsId"); //obsAnswered = obsService.getObservationsAnsweredByConcept(concept); if (ConceptDatatype.NUMERIC.equals(concept.getDatatype().getHl7Abbreviation())) { map.put("displayType", "numeric"); List<Obs> numericAnswers = obsService.getObservations(null, null, Collections.singletonList(concept), null, Collections.singletonList(OpenmrsConstants.PERSON_TYPE.PERSON), null, Collections .singletonList("valueNumeric"), null, null, null, null, false); if (numericAnswers.size() > 0) { Double min = numericAnswers.get(0).getValueNumeric(); Double max = (Double) numericAnswers.get(numericAnswers.size() - 1).getValueNumeric(); Double median = (Double) numericAnswers.get(numericAnswers.size() / 2).getValueNumeric(); Map<Double, Integer> counts = new HashMap<Double, Integer>(); // counts for the histogram Double total = 0.0; // sum of values. used for mean // dataset setup for lineChart TimeSeries timeSeries = new TimeSeries(concept.getName().getName(), Day.class); TimeSeriesCollection timeDataset = new TimeSeriesCollection(); Calendar calendar = Calendar.getInstance(); // array for histogram double[] obsNumerics = new double[(numericAnswers.size())]; Integer i = 0; for (Obs obs : numericAnswers) { Date date = (Date) obs.getObsDatetime(); Double value = (Double) obs.getValueNumeric(); // for mean calculation total += value; // for histogram obsNumerics[i++] = value; Integer count = counts.get(value); counts.put(value, count == null ? 1 : count + 1); // for line chart calendar.setTime(date); Day day = new Day(calendar.get(Calendar.DAY_OF_MONTH), calendar.get(Calendar.MONTH) + 1, // January = 0 calendar.get(Calendar.YEAR) < 1900 ? 1900 : calendar.get(Calendar.YEAR) // jfree chart doesn't like the 19th century ); timeSeries.addOrUpdate(day, value); } Double size = new Double(numericAnswers.size()); Double mean = total / size; map.put("size", numericAnswers.size()); map.put("min", min); map.put("max", max); map.put("mean", mean); map.put("median", median); // create histogram chart HistogramDataset histDataset = new HistogramDataset(); // dataset for histogram histDataset.addSeries(concept.getName().getName(), obsNumerics, counts.size()); JFreeChart histogram = ChartFactory.createHistogram(concept.getName().getName(), msa .getMessage("Concept.stats.histogramDomainAxisTitle"), msa .getMessage("Concept.stats.histogramRangeAxisTitle"), histDataset, PlotOrientation.VERTICAL, false, true, false); map.put("histogram", histogram); if (size > 25) { // calculate 98th percentile of the data: Double x = 0.98; Integer xpercentile = (int) (x * size); Double upperQuartile = numericAnswers.get(xpercentile).getValueNumeric(); Double lowerQuartile = numericAnswers.get((int) (size - xpercentile)).getValueNumeric(); Double innerQuartile = upperQuartile - lowerQuartile; Double innerQuartileLimit = innerQuartile * 1.5; // outliers will be greater than this from the upper/lower quartile Double upperQuartileLimit = upperQuartile + innerQuartileLimit; Double lowerQuartileLimit = lowerQuartile - innerQuartileLimit; List<Obs> outliers = new Vector<Obs>(); // move outliers to the outliers list // removing lower quartile outliers for (i = 0; i < size - xpercentile; i++) { Obs possibleOutlier = numericAnswers.get(i); if (possibleOutlier.getValueNumeric() >= lowerQuartileLimit) break; // quit if this value is greater than the lower limit outliers.add(possibleOutlier); } // removing upper quartile outliers for (i = size.intValue() - 1; i >= xpercentile; i--) { Obs possibleOutlier = numericAnswers.get(i); if (possibleOutlier.getValueNumeric() <= upperQuartileLimit) break; // quit if this value is less than the upper limit outliers.add(possibleOutlier); } numericAnswers.removeAll(outliers); double[] obsNumericsOutliers = new double[(numericAnswers.size())]; i = 0; counts.clear(); for (Obs values : numericAnswers) { Double value = values.getValueNumeric(); obsNumericsOutliers[i++] = value; Integer count = counts.get(value); counts.put(value, count == null ? 1 : count + 1); } // create outlier histogram chart HistogramDataset outlierHistDataset = new HistogramDataset(); outlierHistDataset.addSeries(concept.getName().getName(), obsNumericsOutliers, counts.size()); JFreeChart histogramOutliers = ChartFactory.createHistogram(concept.getName().getName(), msa .getMessage("Concept.stats.histogramDomainAxisTitle"), msa .getMessage("Concept.stats.histogramRangeAxisTitle"), outlierHistDataset, PlotOrientation.VERTICAL, false, true, false); map.put("histogramOutliers", histogramOutliers); map.put("outliers", outliers); } // create line graph chart timeDataset.addSeries(timeSeries); JFreeChart lineChart = ChartFactory.createTimeSeriesChart(concept.getName().getName(), msa .getMessage("Concept.stats.lineChartDomainAxisLabel"), msa .getMessage("Concept.stats.lineChartRangeAxisLabel"), timeDataset, false, true, false); map.put("timeSeries", lineChart); } } else if (ConceptDatatype.BOOLEAN.equals(concept.getDatatype().getHl7Abbreviation())) { // create bar chart for boolean answers map.put("displayType", "boolean"); List<Obs> obs = obsService.getObservations(null, null, Collections.singletonList(concept), null, Collections.singletonList(OpenmrsConstants.PERSON_TYPE.PERSON), null, null, null, null, null, null, false); DefaultPieDataset pieDataset = new DefaultPieDataset(); // count the number of unique answers Map<String, Integer> counts = new HashMap<String, Integer>(); for (Obs o : obs) { Boolean answer = o.getValueAsBoolean(); if (answer == null) answer = false; String name = answer.toString(); Integer count = counts.get(name); counts.put(name, count == null ? 1 : count + 1); } // put the counts into the dataset for (Map.Entry<String, Integer> entry : counts.entrySet()) pieDataset.setValue(entry.getKey(), entry.getValue()); JFreeChart pieChart = ChartFactory.createPieChart(concept.getName().getName(), pieDataset, true, true, false); map.put("pieChart", pieChart); } else if (ConceptDatatype.CODED.equals(concept.getDatatype().getHl7Abbreviation())) { // create pie graph for coded answers map.put("displayType", "coded"); List<Obs> obs = obsService.getObservations(null, null, Collections.singletonList(concept), null, Collections.singletonList(OpenmrsConstants.PERSON_TYPE.PERSON), null, null, null, null, null, null, false); DefaultPieDataset pieDataset = new DefaultPieDataset(); // count the number of unique answers Map<String, Integer> counts = new HashMap<String, Integer>(); for (Obs o : obs) { Concept value = o.getValueCoded(); String name; if (value == null) name = "[value_coded is null]"; else name = value.getName().getName(); Integer count = counts.get(name); counts.put(name, count == null ? 1 : count + 1); } // put the counts into the dataset for (Map.Entry<String, Integer> entry : counts.entrySet()) pieDataset.setValue(entry.getKey(), entry.getValue()); JFreeChart pieChart = ChartFactory.createPieChart(concept.getName().getName(), pieDataset, true, true, false); map.put("pieChart", pieChart); } } } //map.put("obs", obs); //map.put("obsAnswered", obsAnswered); map.put("locale", locale.getLanguage().substring(0, 2)); return map; } }