Jendela dari Bagian 2 masih diam: tombol Muat Data belum berbuat apa pun. Bagian ini menghidupkannya dengan urutan yang sengaja dibalik dari tampilan — logika dulu, penyambungan belakangan. Kita membangun analisisInti() (statistik + korelasi + laporan), dua pemasok datanya (buatDataContoh() dan muatCSV()), lalu menempelkan semuanya ke callback di aplikasiAnalisis.m. Seluruh fungsi logika di bagian ini murni — dan terbukti berjalan di GNU Octave dengan output nyata. Cakupannya:

  • analisisInti: struct masukan → struct hasil + laporan teks,
  • buatDataContoh dan muatCSV: berkas CSV → struct data, lengkap dengan guard,
  • callback ButtonPushedFcn dan ValueChangedFcn: baca → panggil → tulis,
  • jalur galat: try/catch di callback dan dialog uialert.

analisisInti: Jantung yang Teruji #

Fungsi ini menerima struct {data, namaKolom} dan mengembalikan struct hasil berisi statistik tiap kolom, korelasi kolom pertama terhadap kolom lain, dan laporan teks siap tampil. Simpan sebagai src/analisisInti.m:

matlab
function hasil = analisisInti(input)
% ANALISISINTI Menghitung statistik ringkas dari struct data.
%   hasil = analisisInti(input)
%
%   input (struct):
%     input.data      : matriks numerik (baris = observasi, kolom = variabel)
%     input.namaKolom : cell array nama kolom (opsional; bila kosong
%                       dipakai nama bawaan kolom1, kolom2, ...)
%
%   hasil (struct):
%     hasil.jumlahBaris, hasil.jumlahKolom : ukuran data
%     hasil.mean, hasil.stdDev             : rata-rata & deviasi standar per kolom
%     hasil.minNilai, hasil.maksNilai      : nilai terkecil & terbesar per kolom
%     hasil.korelasi                       : korelasi kolom 1 vs kolom 2..n
%     hasil.laporan                        : laporan teks siap ditampilkan

% --- Guard bentuk input ---
if ~isfield(input, 'data') || ~isnumeric(input.data)
    error('analisisInti:input', 'Field "data" harus berupa matriks numerik.');
end
data = input.data;
if isempty(data) || ndims(data) ~= 2
    error('analisisInti:input', 'Field "data" tidak boleh kosong.');
end
nBaris = size(data, 1);
nKolom = size(data, 2);
if nKolom < 2
    error('analisisInti:kolom', ...
        'Data memerlukan minimal 2 kolom, ditemukan %d.', nKolom);
end

% --- Nama kolom: pakai yang diberikan atau nama bawaan ---
if ~isfield(input, 'namaKolom') || isempty(input.namaKolom)
    namaKolom = arrayfun(@(k) sprintf('kolom%d', k), 1:nKolom, ...
        'UniformOutput', false);
elseif iscell(input.namaKolom) && numel(input.namaKolom) == nKolom
    namaKolom = input.namaKolom;
else
    error('analisisInti:namaKolom', ...
        'Jumlah namaKolom tidak sama dengan jumlah kolom data (%d).', nKolom);
end

% --- Statistik per kolom ---
hasil.jumlahBaris = nBaris;
hasil.jumlahKolom = nKolom;
hasil.namaKolom = namaKolom;
hasil.mean = mean(data, 1);
hasil.stdDev = std(data, 0, 1);
hasil.minNilai = min(data, [], 1);
hasil.maksNilai = max(data, [], 1);

% --- Korelasi kolom pertama vs tiap kolom lain ---
x = data(:, 1);
tengahX = x - mean(x);
bawahX = sum(tengahX .^ 2);
korelasi = zeros(1, nKolom - 1);
for k = 2:nKolom
    y = data(:, k);
    tengahY = y - mean(y);
    bawahY = sum(tengahY .^ 2);
    if bawahX == 0 || bawahY == 0
        korelasi(k - 1) = NaN;   % kolom konstan: korelasi tak terdefinisi
    else
        korelasi(k - 1) = sum(tengahX .* tengahY) / sqrt(bawahX * bawahY);
    end
end
hasil.korelasi = korelasi;

% --- Laporan teks (tabel sprintf) ---
lp = sprintf('Laporan Analisis Data (%d baris)\n', nBaris);
lp = [lp, sprintf('%-12s %10s %10s %10s %10s\n', ...
    'Kolom', 'Mean', 'Std', 'Min', 'Maks')];
for k = 1:nKolom
    lp = [lp, sprintf('%-12s %10.3f %10.3f %10.3f %10.3f\n', ...
        namaKolom{k}, hasil.mean(k), hasil.stdDev(k), ...
        hasil.minNilai(k), hasil.maksNilai(k))];
end
lp = [lp, sprintf('\nKorelasi kolom "%s" vs kolom lain:\n', namaKolom{1})];
for k = 2:nKolom
    lp = [lp, sprintf('  %s-%s : %7.3f\n', ...
        namaKolom{1}, namaKolom{k}, korelasi(k - 1))];
end
hasil.laporan = lp;
end

Mari kita bedah baris-baris kode di atas:

  1. Baris 20-33 (guard bentuk): tiga pemeriksaan berurutan — data harus numerik, tidak kosong dan berdimensi dua, dan minimal 2 kolom. Data satu kolom ditolak karena korelasi kolom pertama vs kolom lain tidak punya lawan bicara.
  2. Baris 35-46 (nama kolom): nama diterima bila cell array dengan panjang pas; kosong atau tidak ada → nama bawaan kolom1, kolom2; panjang salah → galat eksplisit. Kontrak yang longgar diam-diam lebih berbahaya daripada galat yang jelas.
  3. Baris 51-57 (statistik per kolom): mean(data, 1) mengoperasi per kolom; std(data, 0, 1) — bendera 0 berarti deviasi standar contoh (pembagi N-1), bawaan statistik MATLAB.
  4. Baris 59-72 (korelasi): korelasi Pearson kolom pertama vs kolom ke-k dihitung manual dari rumus. Kolom konstan membuat penyebut nol → hasil NaN dijaga eksplisit, bukan pembagian nol diam-diam.
  5. Baris 74-92 (laporan): tabel dirakit dengan sprintf: %-12s rata kiri untuk nama, %10.3f rata kanan untuk angka — selaras dengan konvensi kolom MATLAB. Laporan ditempel per baris dengan lp = [lp, ...].

Fungsi ini bisa dicoba saat itu juga tanpa GUI — driver kecil berikut dijalankan dari folder src/ (di Command Window MATLAB maupun Octave):

matlab
% driver sementara: uji lapisan logika tanpa GUI (jalankan dari folder src/)
buatDataContoh();
data = muatCSV('data_contoh.csv');
hasil = analisisInti(data);
fprintf('%s', hasil.laporan);

Output yang Dihasilkan:

text
Laporan Analisis Data (12 baris)
Kolom              Mean        Std        Min       Maks
bulan             6.500      3.606      1.000     12.000
penjualan       291.667     58.517    200.000    380.000
suhu             26.917      1.019     25.500     28.500

Korelasi kolom "bulan" vs kolom lain:
  bulan-penjualan :   0.997
  bulan-suhu :   0.012

Dua angka korelasi itu menyimpan cerita data: bulan-penjualan bernilai 0.997 karena penjualan naik hampir lurus sepanjang tahun, sedangkan bulan-suhu bernilai 0.012 karena suhu memuncak di tengah tahun lalu turun — pola melengkung yang tidak tertangkap korelasi linear. Ringkasan inilah yang akan dibaca pengguna aplikasi kita.

Memasok Data: buatDataContoh dan muatCSV #

buatDataContoh() menulis CSV dengan data tetap 12 baris x 3 kolom. Trik penulisannya satu baris sprintf: format '%d,%d,%.1f\n' diberikan matriks M', dan sprintf membaca argumen matriks kolom demi kolom — setiap kolom M' adalah satu baris data M:

matlab
% Satu baris CSV per baris data: bulan (bulat), penjualan (bulat),
% suhu (satu desimal). sprintf membaca M' kolom demi kolom, dan setiap
% kolom M' adalah satu baris data M.
teks = sprintf('%s,%s,%s\n', namaKolom{:});
teks = [teks, sprintf('%d,%d,%.1f\n', M')];

fid = fopen(namaBerkas, 'w');
if fid == -1
    error('buatDataContoh:gagal', 'Gagal menulis berkas: %s', namaBerkas);
end
fprintf(fid, '%s', teks);
fclose(fid);

Berkas data_contoh.csv yang dihasilkannya:

text
bulan,penjualan,suhu
1,200,26.0
2,215,26.5
3,230,25.5
4,255,27.0
5,270,28.0
6,290,28.5
7,305,28.5
8,320,27.5
9,330,27.0
10,345,26.5
11,360,26.0
12,380,26.0

muatCSV() membaca berkas itu kembali baris demi baris dengan fgetl, memecah tiap baris dengan strsplit, dan mengubah tiap token menjadi angka — sambil menjaga seluruh kemungkinan rusaknya berkas:

matlab
% --- Baris data: angka-angka ---
barisData = [];
nBaris = 0;
while true
    baris = fgetl(fid);
    if ~ischar(baris)            % fgetl mengembalikan -1 di akhir berkas
        break;
    end
    if isempty(strtrim(baris))   % lewati baris kosong
        continue;
    end
    nBaris = nBaris + 1;
    token = strsplit(strtrim(baris), ',');
    if numel(token) ~= nKolom
        fclose(fid);
        error('muatCSV:kolom', ...
            'Baris %d memiliki %d kolom, header memiliki %d.', ...
            nBaris, numel(token), nKolom);
    end
    nilai = zeros(1, nKolom);
    for k = 1:nKolom
        v = str2double(strtrim(token{k}));
        if isnan(v)
            fclose(fid);
            error('muatCSV:angka', ...
                'Baris %d kolom %d ("%s") bukan angka.', ...
                nBaris, k, strtrim(token{k}));
        end
        nilai(k) = v;
    end
    barisData = [barisData; nilai];   %#ok<AGROW>
end
fclose(fid);

Mari kita bedah baris-baris kode di atas:

  1. Baris 4-6 (fgetl + ischar): fgetl mengembalikan teks per baris dan -1 di akhir berkas — pemeriksaan ~ischar adalah penanda berakhirnya loop.
  2. Baris 7-9 (strtrim): baris yang hanya berisi spasi dilewati, bukan dihitung sebagai baris rusak.
  3. Baris 11-15 (numel(token) ~= nKolom): jumlah token harus sama dengan jumlah kolom header — baris kehilangan koma langsung ditolak dengan pesan yang menyebut lokasinya.
  4. Baris 16-23 (str2double + isnan): str2double mengembalikan NaN — bukan galat — untuk teks yang bukan angka, maka isnan adalah pemeriksaannya; pesan galat menyebut baris, kolom, dan isinya.
  5. Baris 24 ([barisData; nilai]): matriks tumbuh satu baris per baris berkas — cukup untuk data bulanan kecil.

Guard lain melengkapi pintu masuk dan keluar: berkas tidak ada (exist), header kosong, header hanya satu kolom, dan tidak ada baris data. Setiap penolakan memakai error dengan pesan berbahasa Indonesia — kontrak muatCSV mengembalikan struct {data, namaKolom} yang persis diterima analisisInti.

Callback: Menyambungkan UI dengan Logika #

Dengan logika siap, callback tombol di aplikasiAnalisis.m tinggal merangkai — MATLAB-only, berjalan hanya di desktop MATLAB:

matlab
    function muatDataButtonPushed(~, ~)
        btnMuat.Enable = 'off';          % cegah klik ganda selama proses
        try
            berkas = 'data_contoh.csv';
            if ~exist(berkas, 'file')
                buatDataContoh(berkas);  % klik pertama membuat data contoh
            end
            dataBaru = muatCSV(berkas);
            dataMatriks = dataBaru.data;
            namaKolomCell = dataBaru.namaKolom;
            hasilAnalisis = analisisInti(dataBaru);

            ddKolom.Items = namaKolomCell(2:end);   % kolom Y = selain kolom X
            ddKolom.Value = namaKolomCell{2};
            ddKolom.Enable = 'on';
            perbaruiTampilan(namaKolomCell{2});
        catch galat
            uialert(fig, galat.message, 'Gagal Memuat Data', 'Icon', 'error');
        end
        btnMuat.Enable = 'on';           % pulihkan tombol apa pun hasilnya
    end

Mari kita bedah baris-baris kode di atas:

  1. Baris 2 (btnMuat.Enable = 'off'): tombol dikunci sebelum bekerja — klik ganda tidak bisa menumpuk dua kali pemuatan; event loop MATLAB memproses callback satu per satu.
  2. Baris 4-6 (exist + buatDataContoh): berkas contoh dibuat hanya bila belum ada, sehingga pengguna bisa mengedit CSV lalu menekan Muat Data ulang untuk memuat versi terbaru.
  3. Baris 7-10 (muat → hitung): dua baris pemanggilan fungsi murni — muatCSV lalu analisisInti — lalu hasilnya disimpan ke state fungsi utama. Callback tidak berisi satu rumus pun.
  4. Baris 12-14 (Items, Value, Enable): dropdown diisi nama kolom selain kolom X (bulan), nilai awal kolom kedua, lalu diaktifkan. Mengisi Value secara programatik tidak memicu ValueChangedFcn — pembaruan tampilan kita panggil sendiri di baris 15.
  5. Baris 16-18 (catch + uialert): galat dari guard muatCSV/analisisInti ditangkap lalu ditampilkan sebagai dialog modal berikon error di depan jendela — pengguna membaca pesan yang jelas, bukan teks galat mentah.
  6. Baris 20 (Enable = 'on'): tombol dipulihkan setelah try/catch — berapa pun hasilnya, aplikasi tidak pernah terkunci permanen.

Memperbarui Tampilan #

Pembantu perbaruiTampilan() dipakai ulang oleh dua callback — tombol dan dropdown — menggambar grafik dan menulis ringkasan untuk kolom Y terpilih:

matlab
    function kolomYChanged(~, ~)
        if isempty(dataMatriks)
            return;   % belum ada data: tidak ada yang perlu digambar
        end
        perbaruiTampilan(ddKolom.Value);
    end

    function perbaruiTampilan(namaKolomY)
        idxY = find(strcmp(namaKolomCell, namaKolomY));

        cla(axGrafik);                   % bersihkan grafik lama
        plot(axGrafik, dataMatriks(:, 1), dataMatriks(:, idxY), ...
            'LineWidth', 1.5, 'Marker', 'o');
        axGrafik.XLabel.String = namaKolomCell{1};
        axGrafik.YLabel.String = namaKolomY;
        axGrafik.Title.String = sprintf('%s vs %s', ...
            namaKolomY, namaKolomCell{1});

        % Ringkasan tiga baris (dokumentasi uilabel: sprintf dengan \n
        % menghasilkan teks multi-baris).
        lblRingkasan.Text = sprintf( ...
            '%d baris | kolom Y: %s\nmean %.1f | std %.1f | min %.1f | maks %.1f\nkorelasi %s-%s: %.3f', ...
            hasilAnalisis.jumlahBaris, namaKolomY, ...
            hasilAnalisis.mean(idxY), hasilAnalisis.stdDev(idxY), ...
            hasilAnalisis.minNilai(idxY), hasilAnalisis.maksNilai(idxY), ...
            namaKolomCell{1}, namaKolomY, ...
            hasilAnalisis.korelasi(idxY - 1));
    end

Tiga keputusan pentingnya: plot(axGrafik, ...) selalu menyebut target axes — tanpa target, MATLAB membuka jendela figure baru di luar aplikasi; cla(axGrafik) membersihkan grafik lama sebelum menggambar; dan label ringkasan diisi sprintf tiga baris yang menyisipkan \n. Nilai-nilai yang dialirkan ke lblRingkasan.Text bisa diperiksa tanpa GUI:

matlab
% teks yang akan diterima lblRingkasan (nilai sama dengan app sungguhan)
lblTeks = sprintf('%d baris | kolom Y: %s\nmean %.1f | std %.1f | min %.1f | maks %.1f\nkorelasi %s-%s: %.3f', ...
    hasil.jumlahBaris, data.namaKolom{2}, hasil.mean(2), hasil.stdDev(2), ...
    hasil.minNilai(2), hasil.maksNilai(2), ...
    data.namaKolom{1}, data.namaKolom{2}, hasil.korelasi(1));
disp(lblTeks)

Output yang Dihasilkan:

text
12 baris | kolom Y: penjualan
mean 291.7 | std 58.5 | min 200.0 | maks 380.0
korelasi bulan-penjualan: 0.997

Saat pengguna memilih suhu, teks yang sama terbentuk ulang dari kolom ketiga: mean 26.9 | std 1.0 | min 25.5 | maks 28.5 dan korelasi bulan-suhu: 0.012 — grafik ikut berganti tanpa membaca berkas lagi.

Jalur Galat di Praktik #

Jalur catch callback terpicu oleh guard-guard muatCSV. Simulasi berikut membuktikan pesannya tanpa GUI:

matlab
% CSV rusak: token "abc" di kolom penjualan
fid = fopen('penjualan_rusak.csv', 'w');
fprintf(fid, 'bulan,penjualan,suhu\n1,abc,26.0\n');
fclose(fid);

try
    muatCSV('penjualan_rusak.csv');
catch galat
    fprintf('Galat ditangkap: %s\n', galat.message);
end

Output yang Dihasilkan:

text
Galat ditangkap: Baris 1 kolom 2 ("abc") bukan angka.

Di aplikasi sungguhan, baris fprintf itu digantikan uialert(fig, galat.message, 'Gagal Memuat Data', 'Icon', 'error'): dialog modal muncul di depan jendela, pengguna membaca lokasi kesalahan, tombol dipulihkan, dan aplikasi tetap hidup untuk dicoba lagi. Aplikasi kini utuh — tugas terakhir kita adalah membuktikan semuanya bekerja: Bagian 4 menguji lapisan logika secara otomatis dan antarmuka secara terencana.