19 package org.sleuthkit.autopsy.timeline.ui.countsview;
21 import com.google.common.collect.ImmutableList;
22 import com.google.common.collect.Lists;
23 import java.util.List;
25 import java.util.function.Function;
26 import javafx.application.Platform;
27 import javafx.beans.Observable;
28 import javafx.beans.binding.BooleanBinding;
29 import javafx.beans.property.SimpleObjectProperty;
30 import javafx.collections.FXCollections;
31 import javafx.concurrent.Task;
32 import javafx.fxml.FXML;
33 import javafx.geometry.Insets;
34 import javafx.scene.Cursor;
35 import javafx.scene.Node;
36 import javafx.scene.chart.CategoryAxis;
37 import javafx.scene.chart.NumberAxis;
38 import javafx.scene.chart.XYChart;
39 import javafx.scene.control.Label;
40 import javafx.scene.control.RadioButton;
41 import javafx.scene.control.ToggleGroup;
42 import javafx.scene.control.Tooltip;
43 import javafx.scene.image.Image;
44 import javafx.scene.image.ImageView;
45 import javafx.scene.layout.BorderPane;
46 import javafx.scene.layout.HBox;
47 import javafx.scene.layout.Pane;
48 import javafx.scene.text.Font;
49 import javafx.scene.text.FontPosture;
50 import javafx.scene.text.FontWeight;
51 import javafx.scene.text.Text;
52 import javafx.scene.text.TextFlow;
53 import org.controlsfx.control.PopOver;
54 import org.joda.time.Interval;
55 import org.openide.util.NbBundle;
86 private final NumberAxis
countAxis =
new NumberAxis();
87 private final CategoryAxis
dateAxis =
new CategoryAxis(FXCollections.<String>observableArrayList());
93 return labelValueString;
98 return dataSeries.stream().flatMap(series -> series.getData().stream())
99 .anyMatch(data -> data.getXValue().equals(value) && data.getYValue().intValue() > 0);
113 "# {0} - scale name",
114 "CountsViewPane.numberOfEvents=Number of Events ({0})"})
122 dateAxis.getTickMarks().addListener((Observable tickMarks) ->
layoutDateLabels());
123 dateAxis.categorySpacingProperty().addListener((Observable spacing) ->
layoutDateLabels());
124 dateAxis.getCategories().addListener((Observable categories) ->
layoutDateLabels());
127 BooleanBinding scaleIsLinear = scaleProp.isEqualTo(
Scale.
LINEAR);
128 countAxis.tickLabelsVisibleProperty().bind(scaleIsLinear);
129 countAxis.tickMarkVisibleProperty().bind(scaleIsLinear);
130 countAxis.minorTickVisibleProperty().bind(scaleIsLinear);
131 scaleProp.addListener(scale -> {
150 return dateAxis.getCategorySpacing();
155 c1.setEffect(applied ?
getChart().getSelectionEffect() : null);
161 for (XYChart.Series<String, Number> series :
dataSeries) {
162 series.getData().clear();
186 return ImmutableList.of();
194 countAxis.setLabel(Bundle.CountsViewPane_numberOfEvents(scaleProp.get().getDisplayName()));
201 "ScaleType.Linear=Linear",
202 "ScaleType.Logarithmic=Logarithmic"})
203 private static enum Scale implements Function<Long, Double> {
205 LINEAR(Bundle.ScaleType_Linear()) {
207 public Double apply(Long inValue) {
208 return inValue.doubleValue();
211 LOGARITHMIC(Bundle.ScaleType_Logarithmic()) {
213 public Double apply(Long inValue) {
214 return Math.log10(inValue) + 1;
226 this.displayName = displayName;
241 return dateAxis.getStartMargin() + dateAxis.getEndMargin();
267 "CountsViewPane.logRadio.text=Logarithmic",
268 "CountsViewPane.scaleLabel.text=Scale:",
269 "CountsViewPane.scaleHelp.label.text=Scales: ",
270 "CountsViewPane.linearRadio.text=Linear",
271 "CountsViewPane.scaleHelpLinear=The linear scale is good for many use cases. When this scale is selected, the height of the bars represents the counts in a linear, one-to-one fashion, and the y-axis is labeled with values. When the range of values is very large, time periods with low counts may have a bar that is too small to see. To help the user detect this, the labels for date ranges with events are bold. To see bars that are too small, there are three options: adjust the window size so that the timeline has more vertical space, adjust the time range shown so that time periods with larger bars are excluded, or adjust the scale setting to logarithmic.",
272 "CountsViewPane.scaleHelpLog=The logarithmic scale represents the number of events in a non-linear way that compresses the difference between large and small numbers. Note that even with the logarithmic scale, an extremely large difference in counts may still produce bars too small to see. In this case the only option may be to filter events to reduce the difference in counts. NOTE: Because the logarithmic scale is applied to each event type separately, the meaning of the height of the combined bar is not intuitive, and to emphasize this, no labels are shown on the y-axis with the logarithmic scale. The logarithmic scale should be used to quickly compare the counts ",
273 "CountsViewPane.scaleHelpLog2=across time within a type, or across types for one time period, but not both.",
274 "CountsViewPane.scaleHelpLog3= The actual counts (available in tooltips or the result viewer) should be used for absolute comparisons. Use the logarithmic scale with care."})
276 assert logRadio != null :
"fx:id=\"logRadio\" was not injected: check your FXML file 'CountsViewSettingsPane.fxml'.";
277 assert linearRadio != null :
"fx:id=\"linearRadio\" was not injected: check your FXML file 'CountsViewSettingsPane.fxml'.";
278 scaleLabel.setText(Bundle.CountsViewPane_scaleLabel_text());
279 linearRadio.setText(Bundle.CountsViewPane_linearRadio_text());
280 logRadio.setText(Bundle.CountsViewPane_logRadio_text());
282 scaleGroup.selectedToggleProperty().addListener((observable, oldToggle, newToggle) -> {
283 if (newToggle == linearRadio) {
285 }
else if (newToggle == logRadio) {
286 scaleProp.set(Scale.LOGARITHMIC);
289 logRadio.setSelected(
true);
292 logImageView.setCursor(Cursor.HAND);
293 logImageView.setOnMouseClicked(clicked -> {
294 Text text =
new Text(Bundle.CountsViewPane_scaleHelpLog());
295 Text text2 =
new Text(Bundle.CountsViewPane_scaleHelpLog2());
296 Font baseFont = text.getFont();
297 text2.setFont(Font.font(baseFont.getFamily(), FontWeight.BOLD, FontPosture.ITALIC, baseFont.getSize()));
298 Text text3 =
new Text(Bundle.CountsViewPane_scaleHelpLog3());
300 Bundle.CountsViewPane_logRadio_text(),
301 logImageView.getImage(),
302 new TextFlow(text, text2, text3));
306 linearImageView.setCursor(Cursor.HAND);
307 linearImageView.setOnMouseClicked(clicked -> {
308 Text text =
new Text(Bundle.CountsViewPane_scaleHelpLinear());
309 text.setWrappingWidth(480);
311 Bundle.CountsViewPane_linearRadio_text(),
312 linearImageView.getImage(), text);
319 CountsViewSettingsPane() {
320 FXMLConstructor.construct(
this,
"CountsViewSettingsPane.fxml");
337 private static void showPopoverHelp(
final Node owner,
final String headerText,
final Image headerImage,
final Node content) {
338 Pane borderPane =
new BorderPane(null, null,
new ImageView(headerImage),
340 new Label(headerText));
341 borderPane.setPadding(
new Insets(10));
342 borderPane.setPrefWidth(500);
344 PopOver popOver =
new PopOver(borderPane);
345 popOver.setDetachable(
false);
346 popOver.setArrowLocation(PopOver.ArrowLocation.TOP_CENTER);
357 "CountsViewPane.loggedTask.name=Updating Counts View",
358 "CountsViewPane.loggedTask.updatingCounts=Populating view"})
362 super(Bundle.CountsViewPane_loggedTask_name(),
true);
372 protected Boolean
call() throws Exception {
384 resetView(Lists.transform(intervals, rangeInfo::formatForTick));
386 updateMessage(Bundle.CountsViewPane_loggedTask_updatingCounts());
388 int numIntervals = intervals.size();
389 Scale activeScale = scaleProp.get();
397 for (
int i = 0; i < numIntervals; i++) {
401 updateProgress(i, numIntervals);
402 final Interval interval = intervals.get(i);
403 int maxPerInterval = 0;
406 Map<EventType, Long> eventCounts = eventsModel.
getEventCounts(interval);
409 for (
final EventType eventType : eventCounts.keySet()) {
414 final Long count = eventCounts.get(eventType);
416 final String intervalCategory = rangeInfo.
formatForTick(interval);
417 final double adjustedCount = activeScale.apply(count);
419 final XYChart.Data<String, Number> dataItem =
420 new XYChart.Data<>(intervalCategory, adjustedCount,
421 new EventCountsChart.ExtraData(interval, eventType, count));
422 Platform.runLater(() ->
getSeries(eventType).getData().add(dataItem));
423 maxPerInterval += adjustedCount;
426 chartMax = Math.max(chartMax, maxPerInterval);
430 double countAxisUpperbound = 1 + chartMax * 1.2;
432 ? Math.pow(10, Math.max(0, Math.floor(Math.log10(chartMax)) - 1))
434 Platform.runLater(() -> {
435 countAxis.setTickUnit(tickUnit);
436 countAxis.setUpperBound(countAxisUpperbound);
443 dateAxis.getCategories().setAll(categories);
final XYChart.Series< X, Y > getSeries(final EventType et)
Task< Boolean > getNewUpdateTask()
final Map< EventType, XYChart.Series< X, Y > > eventTypeToSeriesMap
String formatForTick(Interval interval)
synchronized Interval getTimeRange()
final CategoryAxis dateAxis
static final Logger LOGGER
FilteredEventsModel getEventsModel()
void syncAxisScaleLabel()
boolean hasCustomTimeNavigationControls()
Map< EventType, Long > getEventCounts(Interval timeRange)
static Tooltip getDefaultTooltip()
final synchronized void refresh()
final NumberAxis countAxis
ImmutableList< Node > getSettingsControls()
final ViewMode getViewMode()
ObservableList< NodeType > getSelectedNodes()
static RangeDivisionInfo getRangeDivisionInfo(Interval timeRange)
ImmutableList< Node > getTimeNavigationControls()
final ObservableList< XYChart.Series< X, Y > > dataSeries
final SimpleObjectProperty< Scale > scaleProp
Boolean isTickBold(String value)
String getTickMarkLabel(String labelValueString)
final CategoryAxis getXAxis()
final NumberAxis getYAxis()
final TimeLineController controller
static void showPopoverHelp(final Node owner, final String headerText, final Image headerImage, final Node content)
void setDateValues(List< String > categories)
synchronized static Logger getLogger(String name)
Scale(String displayName)
CountsViewPane(TimeLineController controller)
final void createSeries()
ImageView linearImageView
void applySelectionEffect(Node c1, Boolean applied)
synchronized void layoutDateLabels()
void setChart(ChartType chart)
synchronized List< Interval > getIntervals()