2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
7 * Jalview is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation, either version 3
10 * of the License, or (at your option) any later version.
12 * Jalview is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty
14 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with Jalview. If not, see <http://www.gnu.org/licenses/>.
19 * The Jalview Authors are detailed in the 'AUTHORS' file.
23 import jalview.api.FeatureColourI;
24 import jalview.datamodel.GraphLine;
25 import jalview.datamodel.features.FeatureAttributes;
26 import jalview.schemes.FeatureColour;
27 import jalview.util.MessageManager;
29 import java.awt.Color;
30 import java.awt.Dimension;
31 import java.awt.FlowLayout;
32 import java.awt.event.ActionEvent;
33 import java.awt.event.ActionListener;
34 import java.awt.event.FocusAdapter;
35 import java.awt.event.FocusEvent;
36 import java.awt.event.ItemEvent;
37 import java.awt.event.ItemListener;
38 import java.awt.event.MouseAdapter;
39 import java.awt.event.MouseEvent;
40 import java.util.ArrayList;
41 import java.util.List;
43 import javax.swing.BorderFactory;
44 import javax.swing.BoxLayout;
45 import javax.swing.ButtonGroup;
46 import javax.swing.JCheckBox;
47 import javax.swing.JColorChooser;
48 import javax.swing.JComboBox;
49 import javax.swing.JLabel;
50 import javax.swing.JMenuItem;
51 import javax.swing.JPanel;
52 import javax.swing.JPopupMenu;
53 import javax.swing.JRadioButton;
54 import javax.swing.JSlider;
55 import javax.swing.JTextField;
56 import javax.swing.border.LineBorder;
57 import javax.swing.event.ChangeEvent;
58 import javax.swing.event.ChangeListener;
60 public class FeatureColourChooser extends JalviewDialog
62 private static final int MAX_TOOLTIP_LENGTH = 50;
64 private FeatureRenderer fr;
66 private FeatureColourI cs;
68 private FeatureColourI oldcs;
70 private AlignmentPanel ap;
72 private boolean adjusting = false;
78 private float scaleFactor;
80 private String type = null;
82 private JPanel minColour = new JPanel();
84 private JPanel maxColour = new JPanel();
86 private JPanel noColour = new JPanel();
88 private JComboBox<String> threshold = new JComboBox<>();
90 private JSlider slider = new JSlider();
92 private JTextField thresholdValue = new JTextField(20);
94 private JCheckBox thresholdIsMin = new JCheckBox();
96 private GraphLine threshline;
98 private Color oldmaxColour;
100 private Color oldminColour;
102 private Color oldNoColour;
104 private ActionListener colourEditor = null;
107 * radio buttons to select what to colour by
108 * label, attribute text, score, attribute value
110 private JRadioButton byDescription = new JRadioButton();
112 private JRadioButton byAttributeText = new JRadioButton();
114 private JRadioButton byScore = new JRadioButton();
116 private JRadioButton byAttributeValue = new JRadioButton();
118 private ActionListener changeColourAction;
121 * choice of attribute (if any) for 'colour by text'
123 private JComboBox<String> textAttributeCombo;
126 * choice of attribute (if any) for 'colour by value'
128 private JComboBox<String> valueAttributeCombo;
136 public FeatureColourChooser(FeatureRenderer frender, String theType)
138 this(frender, false, theType);
142 * Constructor, with option to make a blocking dialog (has to complete in the
143 * AWT event queue thread). Currently this option is always set to false.
149 FeatureColourChooser(FeatureRenderer frender, boolean blocking,
155 String title = MessageManager
156 .formatMessage("label.graduated_color_for_params", new String[]
158 initDialogFrame(this, true, blocking, title, 450, 300);
160 slider.addChangeListener(new ChangeListener()
163 public void stateChanged(ChangeEvent evt)
167 thresholdValue.setText((slider.getValue() / scaleFactor) + "");
168 sliderValueChanged();
172 slider.addMouseListener(new MouseAdapter()
175 public void mouseReleased(MouseEvent evt)
178 * only update Overview and/or structure colouring
179 * when threshold slider drag ends (mouse up)
183 ap.paintAlignment(true, true);
188 float mm[] = fr.getMinMax().get(theType)[0];
193 * ensure scale factor allows a scaled range with
194 * 10 integer divisions ('ticks'); if we have got here,
195 * we should expect that max != min
197 scaleFactor = (max == min) ? 1f : 100f / (max - min);
199 oldcs = fr.getFeatureColours().get(theType);
200 if (!oldcs.isSimpleColour())
202 if (oldcs.isAutoScaled())
205 cs = new FeatureColour((FeatureColour) oldcs, min, max);
209 cs = new FeatureColour((FeatureColour) oldcs);
215 * promote original simple color to a graduated color
216 * - by score if there is a score range, else by label
218 Color bl = oldcs.getColour();
223 // original colour becomes the maximum colour
224 cs = new FeatureColour(Color.white, bl, mm[0], mm[1]);
225 cs.setColourByLabel(mm[0] == mm[1]);
227 minColour.setBackground(oldminColour = cs.getMinColour());
228 maxColour.setBackground(oldmaxColour = cs.getMaxColour());
229 noColour.setBackground(oldNoColour = cs.getNoColour());
235 } catch (Exception ex)
237 ex.printStackTrace();
242 * set the initial state of options on screen
244 thresholdIsMin.setSelected(!cs.isAutoScaled());
246 if (cs.isColourByLabel())
248 if (cs.isColourByAttribute())
250 byAttributeText.setSelected(true);
251 textAttributeCombo.setEnabled(true);
252 textAttributeCombo.setSelectedItem(cs.getAttributeName());
256 byDescription.setSelected(true);
257 textAttributeCombo.setEnabled(false);
262 if (cs.isColourByAttribute())
264 byAttributeValue.setSelected(true);
265 String attributeName = cs.getAttributeName();
266 valueAttributeCombo.setSelectedItem(attributeName);
267 valueAttributeCombo.setEnabled(true);
268 setAttributeMinMax(attributeName);
272 byScore.setSelected(true);
273 valueAttributeCombo.setEnabled(false);
277 if (cs.hasThreshold())
279 // initialise threshold slider and selector
280 threshold.setSelectedIndex(cs.isAboveThreshold() ? 1 : 2);
281 slider.setEnabled(true);
282 slider.setValue((int) (cs.getThreshold() * scaleFactor));
283 thresholdValue.setEnabled(true);
284 threshline = new GraphLine((max - min) / 2f, "Threshold",
286 threshline.value = cs.getThreshold();
296 * Configures the initial layout
298 private void jbInit()
300 this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
301 this.setBackground(Color.white);
303 changeColourAction = new ActionListener() {
305 public void actionPerformed(ActionEvent e)
318 JPanel detailsPanel = new JPanel();
319 detailsPanel.setLayout(new BoxLayout(detailsPanel, BoxLayout.Y_AXIS));
321 JPanel colourByTextPanel = initColourByTextPanel();
322 detailsPanel.add(colourByTextPanel);
324 JPanel colourByValuePanel = initColourByValuePanel();
325 detailsPanel.add(colourByValuePanel);
328 * 4 radio buttons select between colour by description, by
329 * attribute text, by score, or by attribute value
331 ButtonGroup bg = new ButtonGroup();
332 bg.add(byDescription);
333 bg.add(byAttributeText);
335 bg.add(byAttributeValue);
337 JPanel okCancelPanel = initOkCancelPanel();
339 this.add(detailsPanel);
340 this.add(okCancelPanel);
344 * Lay out fields for graduated colour by value
348 protected JPanel initColourByValuePanel()
350 JPanel byValuePanel = new JPanel();
351 byValuePanel.setLayout(new BoxLayout(byValuePanel, BoxLayout.Y_AXIS));
352 byValuePanel.setBorder(BorderFactory.createTitledBorder(MessageManager
353 .getString("label.colour_by_value")));
354 byValuePanel.setBackground(Color.white);
357 * first row - choose colour by score or by attribute, choose attribute
359 JPanel byWhatPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
360 byWhatPanel.setBackground(Color.white);
361 byValuePanel.add(byWhatPanel);
363 byScore.setText(MessageManager.getString("label.score"));
364 byWhatPanel.add(byScore);
365 byScore.addActionListener(changeColourAction);
367 byAttributeValue.setText(MessageManager
368 .getString("label.attribute"));
369 byAttributeValue.addActionListener(changeColourAction);
370 byWhatPanel.add(byAttributeValue);
372 List<String> attNames = FeatureAttributes.getInstance().getAttributes(
374 valueAttributeCombo = populateAttributesDropdown(type, attNames,
378 * if no numeric atttibutes found, disable colour by attribute value
380 if (valueAttributeCombo.getItemCount() == 0)
382 byAttributeValue.setEnabled(false);
385 byWhatPanel.add(valueAttributeCombo);
388 * second row - min/max/no colours
390 JPanel colourRangePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
391 colourRangePanel.setBackground(Color.white);
392 byValuePanel.add(colourRangePanel);
394 minColour.setFont(JvSwingUtils.getLabelFont());
395 minColour.setBorder(BorderFactory.createLineBorder(Color.black));
396 minColour.setPreferredSize(new Dimension(40, 20));
397 minColour.setToolTipText(MessageManager.getString("label.min_colour"));
398 minColour.addMouseListener(new MouseAdapter()
401 public void mousePressed(MouseEvent e)
403 if (minColour.isEnabled())
405 minColour_actionPerformed();
410 maxColour.setFont(JvSwingUtils.getLabelFont());
411 maxColour.setBorder(BorderFactory.createLineBorder(Color.black));
412 maxColour.setPreferredSize(new Dimension(40, 20));
413 maxColour.setToolTipText(MessageManager.getString("label.max_colour"));
414 maxColour.addMouseListener(new MouseAdapter()
417 public void mousePressed(MouseEvent e)
419 if (maxColour.isEnabled())
421 maxColour_actionPerformed();
425 maxColour.setBorder(new LineBorder(Color.black));
427 noColour.setFont(JvSwingUtils.getLabelFont());
428 noColour.setBorder(BorderFactory.createLineBorder(Color.black));
429 noColour.setPreferredSize(new Dimension(40, 20));
430 noColour.setToolTipText("Colour if feature has no attribute value");
431 noColour.addMouseListener(new MouseAdapter()
434 public void mousePressed(MouseEvent e)
436 if (e.isPopupTrigger()) // Mac: mouseReleased
438 showNoColourPopup(e);
441 if (noColour.isEnabled())
443 noColour_actionPerformed();
448 public void mouseReleased(MouseEvent e)
450 if (e.isPopupTrigger()) // Windows: mouseReleased
452 showNoColourPopup(e);
458 noColour.setBorder(new LineBorder(Color.black));
460 JLabel minText = new JLabel(MessageManager.getString("label.min"));
461 minText.setFont(JvSwingUtils.getLabelFont());
462 JLabel maxText = new JLabel(MessageManager.getString("label.max"));
463 maxText.setFont(JvSwingUtils.getLabelFont());
464 JLabel noText = new JLabel(MessageManager.getString("label.no_colour"));
465 noText.setFont(JvSwingUtils.getLabelFont());
467 colourRangePanel.add(minText);
468 colourRangePanel.add(minColour);
469 colourRangePanel.add(maxText);
470 colourRangePanel.add(maxColour);
471 colourRangePanel.add(noText);
472 colourRangePanel.add(noColour);
475 * third row - threshold options and value
477 JPanel thresholdPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
478 thresholdPanel.setBackground(Color.white);
479 byValuePanel.add(thresholdPanel);
481 threshold.addActionListener(changeColourAction);
482 threshold.setToolTipText(MessageManager
483 .getString("label.threshold_feature_display_by_score"));
484 threshold.addItem(MessageManager
485 .getString("label.threshold_feature_no_threshold")); // index 0
486 threshold.addItem(MessageManager
487 .getString("label.threshold_feature_above_threshold")); // index 1
488 threshold.addItem(MessageManager
489 .getString("label.threshold_feature_below_threshold")); // index 2
491 thresholdValue.addActionListener(new ActionListener()
494 public void actionPerformed(ActionEvent e)
496 thresholdValue_actionPerformed();
499 thresholdValue.addFocusListener(new FocusAdapter()
502 public void focusLost(FocusEvent e)
504 thresholdValue_actionPerformed();
507 slider.setPaintLabels(false);
508 slider.setPaintTicks(true);
509 slider.setBackground(Color.white);
510 slider.setEnabled(false);
511 slider.setOpaque(false);
512 slider.setPreferredSize(new Dimension(100, 32));
513 slider.setToolTipText(
514 MessageManager.getString("label.adjust_threshold"));
515 thresholdValue.setEnabled(false);
516 thresholdValue.setColumns(7);
518 thresholdPanel.add(threshold);
519 thresholdPanel.add(slider);
520 thresholdPanel.add(thresholdValue);
523 * 4th row - threshold is min / max
525 JPanel isMinMaxPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
526 isMinMaxPanel.setBackground(Color.white);
527 byValuePanel.add(isMinMaxPanel);
528 thresholdIsMin.setBackground(Color.white);
530 .setText(MessageManager.getString("label.threshold_minmax"));
531 thresholdIsMin.setToolTipText(MessageManager
532 .getString("label.toggle_absolute_relative_display_threshold"));
533 thresholdIsMin.addActionListener(changeColourAction);
534 isMinMaxPanel.add(thresholdIsMin);
540 * Show a popup menu with options to make 'no value colour' the same as Min
541 * Colour or Max Colour
545 protected void showNoColourPopup(MouseEvent evt)
547 JPopupMenu pop = new JPopupMenu();
549 JMenuItem copyMin = new JMenuItem(
550 MessageManager.getString("label.min_colour"));
551 copyMin.addActionListener((new ActionListener()
554 public void actionPerformed(ActionEvent e)
556 noColour.setBackground(minColour.getBackground());
562 JMenuItem copyMax = new JMenuItem(
563 MessageManager.getString("label.max_colour"));
564 copyMax.addActionListener((new ActionListener()
567 public void actionPerformed(ActionEvent e)
569 noColour.setBackground(maxColour.getBackground());
575 pop.show(noColour, evt.getX(), evt.getY());
579 * Lay out OK and Cancel buttons
583 protected JPanel initOkCancelPanel()
585 JPanel okCancelPanel = new JPanel();
586 okCancelPanel.setBackground(Color.white);
587 okCancelPanel.add(ok);
588 okCancelPanel.add(cancel);
589 return okCancelPanel;
593 * Lay out Colour by Label and attribute choice elements
597 protected JPanel initColourByTextPanel()
599 JPanel byTextPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
600 byTextPanel.setBackground(Color.white);
601 byTextPanel.setBorder(BorderFactory.createTitledBorder(MessageManager
602 .getString("label.colour_by_text")));
604 byDescription.setText(MessageManager.getString("label.label"));
605 byDescription.setToolTipText(MessageManager
606 .getString("label.colour_by_label_tip"));
607 byDescription.addActionListener(changeColourAction);
608 byTextPanel.add(byDescription);
610 byAttributeText.setText(MessageManager.getString("label.attribute"));
611 byAttributeText.addActionListener(changeColourAction);
612 byTextPanel.add(byAttributeText);
614 List<String> attNames = FeatureAttributes.getInstance().getAttributes(
616 textAttributeCombo = populateAttributesDropdown(type, attNames, false);
617 byTextPanel.add(textAttributeCombo);
620 * disable colour by attribute if no attributes
622 if (attNames.isEmpty())
624 byAttributeText.setEnabled(false);
631 * Action on clicking the 'minimum colour' - open a colour chooser dialog, and
632 * set the selected colour (if the user does not cancel out of the dialog)
634 protected void minColour_actionPerformed()
636 Color col = JColorChooser.showDialog(this,
637 MessageManager.getString("label.select_colour_minimum_value"),
638 minColour.getBackground());
641 minColour.setBackground(col);
642 minColour.setForeground(col);
649 * Action on clicking the 'maximum colour' - open a colour chooser dialog, and
650 * set the selected colour (if the user does not cancel out of the dialog)
652 protected void maxColour_actionPerformed()
654 Color col = JColorChooser.showDialog(this,
655 MessageManager.getString("label.select_colour_maximum_value"),
656 maxColour.getBackground());
659 maxColour.setBackground(col);
660 maxColour.setForeground(col);
667 * Action on clicking the 'no colour' - open a colour chooser dialog, and set
668 * the selected colour (if the user does not cancel out of the dialog)
670 protected void noColour_actionPerformed()
672 Color col = JColorChooser.showDialog(this,
673 MessageManager.getString("label.select_no_value_colour"),
674 noColour.getBackground());
677 noColour.setBackground(col);
678 noColour.setForeground(col);
685 * Constructs and sets the selected colour options as the colour for the
686 * feature type, and repaints the alignment, and optionally the Overview
687 * and/or structure viewer if open
689 * @param updateStructsAndOverview
691 void changeColour(boolean updateStructsAndOverview)
693 // Check if combobox is still adjusting
699 boolean aboveThreshold = false;
700 boolean belowThreshold = false;
701 if (threshold.getSelectedIndex() == 1)
703 aboveThreshold = true;
705 else if (threshold.getSelectedIndex() == 2)
707 belowThreshold = true;
709 boolean hasThreshold = aboveThreshold || belowThreshold;
711 slider.setEnabled(true);
712 thresholdValue.setEnabled(true);
715 * make the feature colour
718 if (cs.isColourByLabel())
720 acg = new FeatureColour(oldminColour, oldmaxColour, min, max);
724 acg = new FeatureColour(oldminColour = minColour.getBackground(),
725 oldmaxColour = maxColour.getBackground(),
726 oldNoColour = noColour.getBackground(), min, max);
728 String attribute = null;
729 textAttributeCombo.setEnabled(false);
730 valueAttributeCombo.setEnabled(false);
731 if (byAttributeText.isSelected())
733 attribute = (String) textAttributeCombo.getSelectedItem();
734 textAttributeCombo.setEnabled(true);
736 else if (byAttributeValue.isSelected())
738 attribute = (String) valueAttributeCombo.getSelectedItem();
739 valueAttributeCombo.setEnabled(true);
741 acg.setAttributeName(attribute);
745 slider.setEnabled(false);
746 thresholdValue.setEnabled(false);
747 thresholdValue.setText("");
748 thresholdIsMin.setEnabled(false);
750 else if (threshline == null)
753 * todo not yet implemented: visual indication of feature threshold
755 threshline = new GraphLine((max - min) / 2f, "Threshold",
762 acg.setThreshold(threshline.value);
764 float range = (max - min) * scaleFactor;
766 slider.setMinimum((int) (min * scaleFactor));
767 slider.setMaximum((int) (max * scaleFactor));
768 // slider.setValue((int) (threshline.value * scaleFactor));
769 slider.setValue(Math.round(threshline.value * scaleFactor));
770 thresholdValue.setText(threshline.value + "");
771 slider.setMajorTickSpacing((int) (range / 10f));
772 slider.setEnabled(true);
773 thresholdValue.setEnabled(true);
774 thresholdIsMin.setEnabled(!byDescription.isSelected());
778 acg.setAboveThreshold(aboveThreshold);
779 acg.setBelowThreshold(belowThreshold);
780 if (thresholdIsMin.isSelected() && hasThreshold)
782 acg.setAutoScaled(false);
785 acg = new FeatureColour((FeatureColour) acg, threshline.value, max);
789 acg = new FeatureColour((FeatureColour) acg, min, threshline.value);
794 acg.setAutoScaled(true);
796 acg.setColourByLabel(byDescription.isSelected()
797 || byAttributeText.isSelected());
799 if (acg.isColourByLabel())
801 maxColour.setEnabled(false);
802 minColour.setEnabled(false);
803 noColour.setEnabled(false);
804 maxColour.setBackground(this.getBackground());
805 maxColour.setForeground(this.getBackground());
806 minColour.setBackground(this.getBackground());
807 minColour.setForeground(this.getBackground());
808 noColour.setBackground(this.getBackground());
809 noColour.setForeground(this.getBackground());
813 maxColour.setEnabled(true);
814 minColour.setEnabled(true);
815 noColour.setEnabled(true);
816 maxColour.setBackground(oldmaxColour);
817 maxColour.setForeground(oldmaxColour);
818 minColour.setBackground(oldminColour);
819 minColour.setForeground(oldminColour);
820 noColour.setBackground(oldNoColour);
821 noColour.setForeground(oldNoColour);
825 * save the colour, and repaint stuff
827 fr.setColour(type, acg);
829 ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview);
833 protected void raiseClosed()
835 if (this.colourEditor != null)
837 colourEditor.actionPerformed(new ActionEvent(this, 0, "CLOSED"));
842 public void okPressed()
848 public void cancelPressed()
854 * Action when the user cancels the dialog. All previous settings should be
855 * restored and rendered on the alignment, and any linked Overview window or
860 fr.setColour(type, oldcs);
861 ap.paintAlignment(true, true);
866 * Action on text entry of a threshold value
868 protected void thresholdValue_actionPerformed()
872 float f = Float.parseFloat(thresholdValue.getText());
873 slider.setValue((int) (f * scaleFactor));
874 threshline.value = f;
877 * force repaint of any Overview window or structure
879 ap.paintAlignment(true, true);
880 } catch (NumberFormatException ex)
886 * Action on change of threshold slider value. This may be done interactively
887 * (by moving the slider), or programmatically (to update the slider after
888 * manual input of a threshold value).
890 protected void sliderValueChanged()
893 * squash rounding errors by forcing min/max of slider to
894 * actual min/max of feature score range
896 int value = slider.getValue();
897 threshline.value = value == slider.getMaximum() ? max
898 : (value == slider.getMinimum() ? min : value / scaleFactor);
899 cs.setThreshold(threshline.value);
902 * repaint alignment, but not Overview or structure,
903 * to avoid overload while dragging the slider
908 void addActionListener(ActionListener graduatedColorEditor)
910 if (colourEditor != null)
913 "IMPLEMENTATION ISSUE: overwriting action listener for FeatureColourChooser");
915 colourEditor = graduatedColorEditor;
919 * Answers the last colour setting selected by user - either oldcs (which may
920 * be a java.awt.Color) or the new GraduatedColor
924 FeatureColourI getLastColour()
934 * A helper method to build the drop-down choice of attributes for a feature.
935 * Where metadata is available with a description for an attribute, that is
936 * added as a tooltip. The list may be restricted to attributes for which we
937 * hold a range of numerical values (so suitable candidates for a graduated
942 * @param withNumericRange
944 protected JComboBox<String> populateAttributesDropdown(
945 String featureType, List<String> attNames,
946 boolean withNumericRange)
948 List<String> validAtts = new ArrayList<>();
949 List<String> tooltips = new ArrayList<>();
951 FeatureAttributes fa = FeatureAttributes.getInstance();
952 for (String attName : attNames)
954 if (withNumericRange)
956 float[] minMax = fa.getMinMax(featureType, attName);
962 validAtts.add(attName);
963 String desc = fa.getDescription(featureType, attName);
964 if (desc != null && desc.length() > MAX_TOOLTIP_LENGTH)
966 desc = desc.substring(0, MAX_TOOLTIP_LENGTH) + "...";
968 tooltips.add(desc == null ? "" : desc);
971 JComboBox<String> attCombo = JvSwingUtils.buildComboWithTooltips(
972 validAtts, tooltips);
974 attCombo.addItemListener(new ItemListener()
977 public void itemStateChanged(ItemEvent e)
979 setAttributeMinMax(attCombo.getSelectedItem().toString());
984 if (validAtts.isEmpty())
986 attCombo.setToolTipText(MessageManager
987 .getString(withNumericRange ? "label.no_numeric_attributes"
988 : "label.no_attributes"));
995 * Updates the min-max range and scale to be that for the given attribute name
997 * @param attributeName
999 protected void setAttributeMinMax(String attributeName)
1001 float[] minMax = FeatureAttributes.getInstance().getMinMax(type,
1007 scaleFactor = (max == min) ? 1f : 100f / (max - min);