Software Engineering Project: Country Data Visualization with Java Swing
Software Engineering Project: Country Data Visualization with Java Swing
In this post, I’ll share insights from my Software Engineering Project (EECS3311), which demonstrates advanced Java development practices through a comprehensive country data visualization application built with Java Swing.
Project Overview
This project represents a full-featured desktop application that visualizes country data using multiple chart types and interactive features. Built as part of the EECS3311 Software Engineering course, it showcases modern Java development practices, design patterns, and comprehensive testing strategies.
Project Requirements
Functional Requirements
- Data Visualization: Display country data using various chart types
- Interactive Features: User-friendly interface with interactive elements
- Data Management: Load, process, and manage country datasets
- Chart Types: Support multiple visualization formats
- Year Selection: Filter data by different time periods
- Export Functionality: Export charts and data
Non-Functional Requirements
- Performance: Efficient data processing and rendering
- Usability: Intuitive user interface design
- Maintainability: Clean, well-documented code
- Testability: Comprehensive test coverage
- Scalability: Support for large datasets
Technical Architecture
Project Structure
3311project/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ ├── controller/
│ │ │ ├── model/
│ │ │ ├── view/
│ │ │ ├── service/
│ │ │ └── util/
│ │ └── resources/
│ │ ├── data/
│ │ └── images/
│ └── test/
│ ├── java/
│ └── resources/
├── lib/
├── docs/
└── README.mdDesign Patterns Implementation
Model-View-Controller (MVC)
// Model: CountryData.java
public class CountryData {
private String countryName;
private String countryCode;
private Map<Integer, Double> yearlyData;
private DataType dataType;
public CountryData(String name, String code, DataType type) {
this.countryName = name;
this.countryCode = code;
this.dataType = type;
this.yearlyData = new HashMap<>();
}
public void addYearlyData(int year, double value) {
yearlyData.put(year, value);
}
public double getDataForYear(int year) {
return yearlyData.getOrDefault(year, 0.0);
}
// Getters and setters
public String getCountryName() { return countryName; }
public String getCountryCode() { return countryCode; }
public DataType getDataType() { return dataType; }
public Map<Integer, Double> getYearlyData() { return yearlyData; }
}
// View: MainFrame.java
public class MainFrame extends JFrame {
private ChartPanel chartPanel;
private ControlPanel controlPanel;
private DataTablePanel dataTablePanel;
private StatusBar statusBar;
public MainFrame() {
initializeComponents();
setupLayout();
setupEventHandlers();
}
private void initializeComponents() {
chartPanel = new ChartPanel();
controlPanel = new ControlPanel();
dataTablePanel = new DataTablePanel();
statusBar = new StatusBar();
}
private void setupLayout() {
setLayout(new BorderLayout());
add(chartPanel, BorderLayout.CENTER);
add(controlPanel, BorderLayout.NORTH);
add(dataTablePanel, BorderLayout.EAST);
add(statusBar, BorderLayout.SOUTH);
}
public void updateChart(List<CountryData> data) {
chartPanel.updateChart(data);
}
public void updateDataTable(List<CountryData> data) {
dataTablePanel.updateData(data);
}
}
// Controller: MainController.java
public class MainController {
private MainFrame view;
private DataService dataService;
private ChartService chartService;
public MainController(MainFrame view) {
this.view = view;
this.dataService = new DataService();
this.chartService = new ChartService();
setupEventHandlers();
}
private void setupEventHandlers() {
view.getControlPanel().addYearChangeListener(this::onYearChanged);
view.getControlPanel().addChartTypeChangeListener(this::onChartTypeChanged);
view.getControlPanel().addCountrySelectionListener(this::onCountrySelectionChanged);
}
private void onYearChanged(int year) {
List<CountryData> data = dataService.getDataForYear(year);
view.updateChart(data);
view.updateDataTable(data);
}
private void onChartTypeChanged(ChartType chartType) {
chartService.setChartType(chartType);
view.refreshChart();
}
private void onCountrySelectionChanged(List<String> selectedCountries) {
List<CountryData> filteredData = dataService.getDataForCountries(selectedCountries);
view.updateChart(filteredData);
}
}Strategy Pattern for Chart Types
// Chart Strategy Interface
public interface ChartStrategy {
void drawChart(Graphics2D g2d, List<CountryData> data, Dimension size);
String getChartTypeName();
}
// Bar Chart Implementation
public class BarChartStrategy implements ChartStrategy {
@Override
public void drawChart(Graphics2D g2d, List<CountryData> data, Dimension size) {
if (data.isEmpty()) return;
int barWidth = size.width / data.size();
int maxValue = (int) data.stream()
.mapToDouble(CountryData::getMaxValue)
.max()
.orElse(1);
for (int i = 0; i < data.size(); i++) {
CountryData country = data.get(i);
int barHeight = (int) ((country.getMaxValue() / maxValue) * size.height);
g2d.setColor(getColorForCountry(country.getCountryName()));
g2d.fillRect(i * barWidth, size.height - barHeight, barWidth - 2, barHeight);
// Draw country name
g2d.setColor(Color.BLACK);
g2d.drawString(country.getCountryName(),
i * barWidth, size.height + 15);
}
}
@Override
public String getChartTypeName() {
return "Bar Chart";
}
}
// Line Chart Implementation
public class LineChartStrategy implements ChartStrategy {
@Override
public void drawChart(Graphics2D g2d, List<CountryData> data, Dimension size) {
if (data.isEmpty()) return;
Map<String, Color> countryColors = generateCountryColors(data);
for (CountryData country : data) {
g2d.setColor(countryColors.get(country.getCountryName()));
g2d.setStroke(new BasicStroke(2.0f));
List<Integer> years = new ArrayList<>(country.getYearlyData().keySet());
Collections.sort(years);
for (int i = 0; i < years.size() - 1; i++) {
int x1 = (int) ((double) i / (years.size() - 1) * size.width);
int y1 = size.height - (int) ((country.getDataForYear(years.get(i)) /
country.getMaxValue()) * size.height);
int x2 = (int) ((double) (i + 1) / (years.size() - 1) * size.width);
int y2 = size.height - (int) ((country.getDataForYear(years.get(i + 1)) /
country.getMaxValue()) * size.height);
g2d.drawLine(x1, y1, x2, y2);
}
}
}
@Override
public String getChartTypeName() {
return "Line Chart";
}
}
// Chart Context
public class ChartContext {
private ChartStrategy strategy;
public void setStrategy(ChartStrategy strategy) {
this.strategy = strategy;
}
public void drawChart(Graphics2D g2d, List<CountryData> data, Dimension size) {
if (strategy != null) {
strategy.drawChart(g2d, data, size);
}
}
public String getChartTypeName() {
return strategy != null ? strategy.getChartTypeName() : "No Chart";
}
}Observer Pattern for UI Updates
// Observer Interface
public interface DataObserver {
void onDataChanged(List<CountryData> newData);
void onYearChanged(int newYear);
void onChartTypeChanged(ChartType newType);
}
// Observable Data Service
public class DataService {
private List<CountryData> currentData;
private int currentYear;
private List<DataObserver> observers;
public DataService() {
this.observers = new ArrayList<>();
this.currentData = new ArrayList<>();
this.currentYear = 2020;
}
public void addObserver(DataObserver observer) {
observers.add(observer);
}
public void removeObserver(DataObserver observer) {
observers.remove(observer);
}
private void notifyDataChanged() {
for (DataObserver observer : observers) {
observer.onDataChanged(new ArrayList<>(currentData));
}
}
private void notifyYearChanged() {
for (DataObserver observer : observers) {
observer.onYearChanged(currentYear);
}
}
public void setYear(int year) {
this.currentYear = year;
loadDataForYear(year);
notifyYearChanged();
notifyDataChanged();
}
public void loadDataForYear(int year) {
// Load data from file or database
currentData = DataLoader.loadCountryData(year);
}
}Data Management
Data Loading Service
public class DataLoader {
private static final String DATA_FILE_PATH = "src/main/resources/data/country_data.csv";
public static List<CountryData> loadCountryData(int year) {
List<CountryData> data = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new FileReader(DATA_FILE_PATH))) {
String line;
String[] headers = reader.readLine().split(",");
while ((line = reader.readLine()) != null) {
String[] values = line.split(",");
if (values.length >= headers.length) {
CountryData countryData = parseCountryData(values, headers, year);
if (countryData != null) {
data.add(countryData);
}
}
}
} catch (IOException e) {
System.err.println("Error loading data: " + e.getMessage());
}
return data;
}
private static CountryData parseCountryData(String[] values, String[] headers, int year) {
try {
String countryName = values[0];
String countryCode = values[1];
CountryData countryData = new CountryData(countryName, countryCode, DataType.POPULATION);
// Parse yearly data
for (int i = 2; i < values.length && i < headers.length; i++) {
try {
int dataYear = Integer.parseInt(headers[i]);
double value = Double.parseDouble(values[i]);
countryData.addYearlyData(dataYear, value);
} catch (NumberFormatException e) {
// Skip invalid data
}
}
return countryData;
} catch (Exception e) {
return null;
}
}
}Data Validation
public class DataValidator {
public static boolean isValidCountryData(CountryData data) {
if (data == null) return false;
if (data.getCountryName() == null || data.getCountryName().trim().isEmpty()) return false;
if (data.getCountryCode() == null || data.getCountryCode().trim().isEmpty()) return false;
if (data.getYearlyData() == null || data.getYearlyData().isEmpty()) return false;
// Validate data values
for (Double value : data.getYearlyData().values()) {
if (value == null || value < 0) return false;
}
return true;
}
public static List<CountryData> filterValidData(List<CountryData> dataList) {
return dataList.stream()
.filter(DataValidator::isValidCountryData)
.collect(Collectors.toList());
}
}User Interface Components
Custom Chart Panel
public class ChartPanel extends JPanel {
private ChartContext chartContext;
private List<CountryData> currentData;
private ChartType currentChartType;
public ChartPanel() {
this.chartContext = new ChartContext();
this.currentData = new ArrayList<>();
this.currentChartType = ChartType.BAR;
setPreferredSize(new Dimension(800, 600));
setBackground(Color.WHITE);
setBorder(BorderFactory.createTitledBorder("Country Data Visualization"));
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
// Enable anti-aliasing
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
// Draw chart
Dimension chartSize = new Dimension(getWidth() - 100, getHeight() - 100);
chartContext.drawChart(g2d, currentData, chartSize);
// Draw title
g2d.setColor(Color.BLACK);
g2d.setFont(new Font("Arial", Font.BOLD, 16));
String title = chartContext.getChartTypeName() + " - Country Data";
FontMetrics fm = g2d.getFontMetrics();
int titleWidth = fm.stringWidth(title);
g2d.drawString(title, (getWidth() - titleWidth) / 2, 30);
g2d.dispose();
}
public void updateChart(List<CountryData> data) {
this.currentData = new ArrayList<>(data);
repaint();
}
public void setChartType(ChartType chartType) {
this.currentChartType = chartType;
switch (chartType) {
case BAR:
chartContext.setStrategy(new BarChartStrategy());
break;
case LINE:
chartContext.setStrategy(new LineChartStrategy());
break;
case PIE:
chartContext.setStrategy(new PieChartStrategy());
break;
}
repaint();
}
}Control Panel
public class ControlPanel extends JPanel {
private JComboBox<Integer> yearComboBox;
private JComboBox<ChartType> chartTypeComboBox;
private JList<String> countryList;
private JButton refreshButton;
private JButton exportButton;
private List<YearChangeListener> yearChangeListeners;
private List<ChartTypeChangeListener> chartTypeChangeListeners;
private List<CountrySelectionListener> countrySelectionListeners;
public ControlPanel() {
this.yearChangeListeners = new ArrayList<>();
this.chartTypeChangeListeners = new ArrayList<>();
this.countrySelectionListeners = new ArrayList<>();
initializeComponents();
setupLayout();
setupEventHandlers();
}
private void initializeComponents() {
// Year selection
yearComboBox = new JComboBox<>();
for (int year = 2010; year <= 2023; year++) {
yearComboBox.addItem(year);
}
yearComboBox.setSelectedItem(2020);
// Chart type selection
chartTypeComboBox = new JComboBox<>(ChartType.values());
chartTypeComboBox.setSelectedItem(ChartType.BAR);
// Country list
countryList = new JList<>();
countryList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
countryList.setVisibleRowCount(10);
// Buttons
refreshButton = new JButton("Refresh");
exportButton = new JButton("Export Chart");
}
private void setupLayout() {
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
// Year selection
gbc.gridx = 0; gbc.gridy = 0;
add(new JLabel("Year:"), gbc);
gbc.gridx = 1;
add(yearComboBox, gbc);
// Chart type selection
gbc.gridx = 0; gbc.gridy = 1;
add(new JLabel("Chart Type:"), gbc);
gbc.gridx = 1;
add(chartTypeComboBox, gbc);
// Country selection
gbc.gridx = 0; gbc.gridy = 2;
add(new JLabel("Countries:"), gbc);
gbc.gridx = 1;
add(new JScrollPane(countryList), gbc);
// Buttons
gbc.gridx = 0; gbc.gridy = 3;
add(refreshButton, gbc);
gbc.gridx = 1;
add(exportButton, gbc);
}
private void setupEventHandlers() {
yearComboBox.addActionListener(e -> {
int selectedYear = (Integer) yearComboBox.getSelectedItem();
notifyYearChanged(selectedYear);
});
chartTypeComboBox.addActionListener(e -> {
ChartType selectedType = (ChartType) chartTypeComboBox.getSelectedItem();
notifyChartTypeChanged(selectedType);
});
countryList.addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) {
List<String> selectedCountries = countryList.getSelectedValuesList();
notifyCountrySelectionChanged(selectedCountries);
}
});
refreshButton.addActionListener(e -> notifyRefreshRequested());
exportButton.addActionListener(e -> notifyExportRequested());
}
// Event notification methods
private void notifyYearChanged(int year) {
for (YearChangeListener listener : yearChangeListeners) {
listener.onYearChanged(year);
}
}
private void notifyChartTypeChanged(ChartType chartType) {
for (ChartTypeChangeListener listener : chartTypeChangeListeners) {
listener.onChartTypeChanged(chartType);
}
}
private void notifyCountrySelectionChanged(List<String> countries) {
for (CountrySelectionListener listener : countrySelectionListeners) {
listener.onCountrySelectionChanged(countries);
}
}
// Listener registration methods
public void addYearChangeListener(YearChangeListener listener) {
yearChangeListeners.add(listener);
}
public void addChartTypeChangeListener(ChartTypeChangeListener listener) {
chartTypeChangeListeners.add(listener);
}
public void addCountrySelectionListener(CountrySelectionListener listener) {
countrySelectionListeners.add(listener);
}
}Testing Strategy
Unit Testing with JUnit 5
@ExtendWith(MockitoExtension.class)
class CountryDataTest {
@Test
@DisplayName("Should create CountryData with valid parameters")
void testCreateCountryData() {
// Given
String countryName = "Canada";
String countryCode = "CA";
DataType dataType = DataType.POPULATION;
// When
CountryData countryData = new CountryData(countryName, countryCode, dataType);
// Then
assertThat(countryData.getCountryName()).isEqualTo(countryName);
assertThat(countryData.getCountryCode()).isEqualTo(countryCode);
assertThat(countryData.getDataType()).isEqualTo(dataType);
assertThat(countryData.getYearlyData()).isEmpty();
}
@Test
@DisplayName("Should add yearly data correctly")
void testAddYearlyData() {
// Given
CountryData countryData = new CountryData("Canada", "CA", DataType.POPULATION);
int year = 2020;
double value = 1000000.0;
// When
countryData.addYearlyData(year, value);
// Then
assertThat(countryData.getDataForYear(year)).isEqualTo(value);
assertThat(countryData.getYearlyData()).hasSize(1);
}
@Test
@DisplayName("Should return default value for non-existent year")
void testGetDataForNonExistentYear() {
// Given
CountryData countryData = new CountryData("Canada", "CA", DataType.POPULATION);
// When
double value = countryData.getDataForYear(2020);
// Then
assertThat(value).isEqualTo(0.0);
}
}
@ExtendWith(MockitoExtension.class)
class DataServiceTest {
@Mock
private DataLoader dataLoader;
@InjectMocks
private DataService dataService;
@Test
@DisplayName("Should load data for specific year")
void testLoadDataForYear() {
// Given
int year = 2020;
List<CountryData> expectedData = Arrays.asList(
new CountryData("Canada", "CA", DataType.POPULATION),
new CountryData("USA", "US", DataType.POPULATION)
);
when(dataLoader.loadCountryData(year)).thenReturn(expectedData);
// When
List<CountryData> actualData = dataService.getDataForYear(year);
// Then
assertThat(actualData).hasSize(2);
assertThat(actualData).containsExactlyElementsOf(expectedData);
verify(dataLoader).loadCountryData(year);
}
@Test
@DisplayName("Should filter data for selected countries")
void testGetDataForCountries() {
// Given
List<String> selectedCountries = Arrays.asList("Canada", "USA");
List<CountryData> allData = Arrays.asList(
new CountryData("Canada", "CA", DataType.POPULATION),
new CountryData("USA", "US", DataType.POPULATION),
new CountryData("Mexico", "MX", DataType.POPULATION)
);
when(dataLoader.loadCountryData(anyInt())).thenReturn(allData);
// When
List<CountryData> filteredData = dataService.getDataForCountries(selectedCountries);
// Then
assertThat(filteredData).hasSize(2);
assertThat(filteredData.stream().map(CountryData::getCountryName))
.containsExactly("Canada", "USA");
}
}Property-Based Testing with Randoop
public class RandoopTestGenerator {
public static void generateTests() {
// Configure Randoop
RandoopConfiguration config = new RandoopConfiguration();
config.setClassList(Arrays.asList(
CountryData.class,
DataService.class,
ChartService.class
));
config.setOutputDir("src/test/java/generated");
config.setTestPackage("generated");
config.setTimeLimit(300); // 5 minutes
// Generate tests
RandoopMain.main(new String[]{
"--classlist=src/test/resources/classlist.txt",
"--output-limit=100",
"--time-limit=300"
});
}
}Integration Testing
@SpringBootTest
class ApplicationIntegrationTest {
@Autowired
private MainController mainController;
@Autowired
private DataService dataService;
@Test
@DisplayName("Should load application and display initial data")
void testApplicationStartup() {
// Given
MainFrame mainFrame = new MainFrame();
MainController controller = new MainController(mainFrame);
// When
controller.initialize();
// Then
assertThat(mainFrame.isVisible()).isTrue();
assertThat(dataService.getCurrentData()).isNotEmpty();
}
@Test
@DisplayName("Should update chart when year changes")
void testYearChangeUpdatesChart() {
// Given
MainFrame mainFrame = new MainFrame();
MainController controller = new MainController(mainFrame);
controller.initialize();
// When
controller.onYearChanged(2021);
// Then
List<CountryData> data = dataService.getDataForYear(2021);
assertThat(data).isNotEmpty();
// Verify chart was updated
}
}Performance Optimization
Data Caching
public class DataCache {
private static final int MAX_CACHE_SIZE = 100;
private final Map<String, List<CountryData>> cache;
private final Map<String, Long> cacheTimestamps;
public DataCache() {
this.cache = new LinkedHashMap<String, List<CountryData>>() {
@Override
protected boolean removeEldestEntry(Map.Entry<String, List<CountryData>> eldest) {
return size() > MAX_CACHE_SIZE;
}
};
this.cacheTimestamps = new HashMap<>();
}
public List<CountryData> get(String key) {
if (isExpired(key)) {
cache.remove(key);
cacheTimestamps.remove(key);
return null;
}
return cache.get(key);
}
public void put(String key, List<CountryData> data) {
cache.put(key, data);
cacheTimestamps.put(key, System.currentTimeMillis());
}
private boolean isExpired(String key) {
Long timestamp = cacheTimestamps.get(key);
if (timestamp == null) return true;
long currentTime = System.currentTimeMillis();
long cacheExpiryTime = 5 * 60 * 1000; // 5 minutes
return (currentTime - timestamp) > cacheExpiryTime;
}
}Lazy Loading
public class LazyDataLoader {
private final Map<Integer, CompletableFuture<List<CountryData>>> loadingTasks;
public LazyDataLoader() {
this.loadingTasks = new ConcurrentHashMap<>();
}
public CompletableFuture<List<CountryData>> loadDataAsync(int year) {
return loadingTasks.computeIfAbsent(year, this::createLoadingTask);
}
private CompletableFuture<List<CountryData>> createLoadingTask(int year) {
return CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(100); // Simulate loading time
return DataLoader.loadCountryData(year);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
});
}
}Lessons Learned
Software Engineering Principles
- Design Patterns: Proper use of design patterns improves code maintainability
- Separation of Concerns: Clear separation between model, view, and controller
- Testing: Comprehensive testing ensures code reliability
- Documentation: Good documentation improves code understanding
Java Development
- Swing Framework: Understanding Swing components and event handling
- Graphics Programming: Custom painting and chart rendering
- Data Management: Efficient data loading and processing
- Performance: Optimization techniques for better user experience
Project Management
- Requirements Analysis: Clear understanding of project requirements
- Iterative Development: Incremental development and testing
- Code Review: Regular code review improves code quality
- Version Control: Proper use of Git for version management
Future Enhancements
Advanced Features
- Real-time Data: Integration with live data sources
- Advanced Charts: More chart types and customization options
- Data Export: Export to various formats (PDF, Excel, etc.)
- User Preferences: Save and load user preferences
Technical Improvements
- Database Integration: Replace file-based storage with database
- Web Interface: Create web-based version using JavaFX or web technologies
- API Integration: Connect to external data APIs
- Performance: Further optimization for large datasets
Conclusion
The Software Engineering project demonstrates comprehensive Java development practices and software engineering principles. Key achievements include:
- Design Patterns: Implementation of MVC, Strategy, and Observer patterns
- User Interface: Professional desktop application with Java Swing
- Data Visualization: Multiple chart types with interactive features
- Testing: Comprehensive testing with JUnit and Randoop
- Code Quality: Clean, maintainable, and well-documented code
The project is available on GitHub and serves as a comprehensive example of Java desktop application development.
This project represents my exploration into software engineering principles and Java desktop application development. The lessons learned here continue to influence my approach to software design, testing, and project management.