0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[僕のlonicでスマホアプリ開発]#7 残高グラフ画面

Posted at

はじめに

前回の[[僕のlonicでスマホアプリ開発]#6 データインポート/エクスポート画面作成(https://qiita.com/SSKNOK/items/2e71535b154df4670a80)続きになります。
今回作成するスマホアプリは画面が以下の通りになる想定ですが、

  • 残高確認画面
  • 残高登録画面
  • 口座登録画面
  • データインポート/エクスポート画面

今回は残高グラフ画面を実装します。

「いや、そんな画面無いじゃん」って感じですが、隠し機能って感じですね。
前回開発した残高確認画面にくっついてる「グラフ」ボタンを押下したら遷移して画面を表示するイメージ。

残高画面開発

入力項目

入力項目は特に想定してないですが、パスパラメータで

  • 銀行名
  • 口座名義人名
  • 検索日FROM
  • 検索日TO
    を受け取ります。

詰まったポイント

パスパラメータ

詰まったポイントではないんですが、他の画面と違って画面遷移時のパラメータがあるのがちょい癖ポイント1つ目です。
何か他に良いパラメータの渡し方はなかったのか。
パラメータを渡すときはvue-routerindex.jsにパスパラメータの定義を行います。

src/router/index.js
import { createRouter, createWebHistory } from '@ionic/vue-router';
import TabsPage from '../views/TabsPage.vue'

const routes = [
  {
    path: '/',
    redirect: '/tabs/view'
  },
  {
    path: '/tabs/',
    component: TabsPage,
    children: [
      {
        path: '',
        redirect: '/tabs/view'
      },
      {
        path: 'view',
        component: () => import('@/views/View.vue')
      },
      {
        path: 'balance/:balanceNo',
        component: () => import('@/views/Balance.vue')
      },
      {
        path: 'account',
        component: () => import('@/views/Account.vue')
      },
      {
        path: 'importExport',
        component: () => import('@/views/importExport.vue')
      },
      {
        path: 'graph/:bank/:accountHolder/:fromDate/:toDate', //←ココ
        component: () => import('@/views/Graph.vue')
      }
    ]
  }
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})

export default router

グラフ描画処理

createGraphで描画するデータをコネコネして、最後にdescribeGraphに渡すことでグラフを描画しています。
グラフの描画にはChart.jsを使っています。
Chart.jsはとりあえずグラフが描画できればそれでよかったので、あんま深く調べず、コチラをまんまコピって使いました。

src/views/Graph.vue
<script setup>
/* ----------------------------------
method:グラフ作成。
detail:グラフに必用なデータを取得し、グラフを描画する。
 ---------------------------------- */
const createGraph = function (bank, accountHolder, fromDate, toDate) {
  // 検索条件の組み立て
  // 銀行
  let searchBank = checkEmptyValue(bank)
    ? SEARCH_ALL
    : SEARCH_TEMPLATE.replaceAll("{keyWord}", bank);
  // 口座名義人
  let searchAccountHolder = checkEmptyValue(accountHolder)
    ? SEARCH_ALL
    : SEARCH_TEMPLATE.replaceAll("{keyWord}", accountHolder); // 口座名義人
  // 対象日(FROM)
  let searchFromDate = checkEmptyValue(fromDate) ? MINIMUM_FROM_DATE : fromDate;
  // 対象日(TO)
  let searchToDate = checkEmptyValue(toDate) ? MAXIMUM_TO_DATE : toDate;

  // 口座情報のリストを生成
  let accountList = [];
  storage.forEach((value, key) => {
    let infoJson = JSON.parse(value);
    // データタイプがaccountInfoなら口座情報として読み込む
    if (infoJson.dataType === DATA_TYPE_FOR_ACCOUNT) {
      accountList.push({
        key: key,
        bank: infoJson.bank,
        accountHolder: infoJson.accountHolder,
      });
    }
  });

  // 日付別に残高を合算
  const recordDataMap = new Map();
  storage
    .forEach((value, key) => {
      let infoJson = JSON.parse(value);

      // accountKeyから口座情報を取得して銀行と口座名義人を取得
      let accountInfoBank = "";
      let accountInfoAccountHolder = "";
      accountList.forEach((account) => {
        if (account.key === infoJson.accountKey) {
          accountInfoBank = account.bank;
          accountInfoAccountHolder = account.accountHolder;
          return true;
        }
      });

      // データタイプがbalanceInfoなら残高情報として読み込む
      if (infoJson.dataType === DATA_TYPE_FOR_BALANCE) {
        let balanceData = {
          key: key,
          accountInfoBank: accountInfoBank,
          no: infoJson.no,
          recordDate: infoJson.recordDate,
          balance: infoJson.balance,
          bank: accountInfoBank,
          accountHolder: accountInfoAccountHolder,
        };
        // 表示対象の場合は日付別に合算する。
        if (
          checkBalanceInfoMatchCondition(
            balanceData,
            searchBank,
            searchAccountHolder,
            searchFromDate,
            searchToDate
          )
        ) {
          if (recordDataMap.has(balanceData.recordDate)) {
            let newBalance =
              Number(infoJson.balance) +
              Number(recordDataMap.get(balanceData.recordDate));
            recordDataMap.set(balanceData.recordDate, newBalance);
          } else {
            recordDataMap.set(balanceData.recordDate, infoJson.balance);
          }
        }
      }
    })
    .then(() => {
      let graphData = []; // 描画用のデータ
      recordDataMap.forEach((value, key) => {
        graphData.push({ x: key, y: value });
      });

      //データを時系列順に並べ替え
      graphData.sort(function (first, second) {
        const firstRecordDate = new Date(first.x);
        const secondRecordDate = new Date(second.x);
        if (firstRecordDate < secondRecordDate) {
          return -1;
        } else if (firstRecordDate > secondRecordDate) {
          return 1;
        } else {
          return 0;
        }
      });

      describeGraph(graphData);
    });
};

/* ----------------------------------
method:グラフ描画
detail:受け取った値のリストに対するグラフを描画する。
 ---------------------------------- */
const describeGraph = function (data) {
  // test
  new Chart("chart", {
    type: "line",
    data: {
      datasets: [
        {
          label: "BALANCE GRAPH",
          backgroundColor: "rgb(255, 99, 132)",
          borderColor: "rgb(255, 99, 132)",
          fill: false,
          data: data,
        },
      ],
    },
    options: {
      aspectRatio: 0.6,
      scales: {
        xAxes: [
          {
            type: "time",
            time: {
              unit: "day",
            },
          },
        ],
      },
    },
  });
};
</script>

ソース全量

Account.vue全量
src/views/Graph.vue
<template>
  <ion-page>
    <ion-header>
      <ion-toolbar>
        <ion-title>GRAPH</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content :fullscreen="true">
      <ion-header collapse="condense">
        <ion-toolbar>
          <ion-title size="large">GRAPH</ion-title>
        </ion-toolbar>
      </ion-header>
      <canvas id="chart"></canvas>
    </ion-content>
  </ion-page>
</template>

<script setup>
import {
  IonPage,
  IonHeader,
  IonToolbar,
  IonTitle,
  IonContent,
  IonGrid,
  IonRow,
  IonCol,
  IonIcon,
  IonButton,
  IonItem,
  IonTextarea,
  onIonViewWillEnter,
  alertController,
} from "@ionic/vue";
import { arrowDownOutline, arrowUpOutline } from "ionicons/icons";
import ExploreContainer from "@/components/ExploreContainer.vue";
import { useRoute } from "vue-router";
import { ref, onMounted } from "vue";
import { Storage } from "@ionic/storage";
import { Chart, registerables } from "chart.js";

/* ******** プロパティ ******** */
let storage; // ストレージオブジェクト

const DATA_TYPE_FOR_ACCOUNT = "accountInfo"; // ストレージのデータタイプ(口座情報)
const KEY_PREFIX_FOR_ACCOUNT = "accountInfo_"; // ストレージのキーの接頭辞(口座情報)
const DATA_TYPE_FOR_BALANCE = "balanceInfo"; // ストレージのデータタイプ(残高情報)
const KEY_PREFIX_FOR_BALANCE = "balanceInfo_"; // ストレージのキーの接頭辞(残高情報)
const SEARCH_TEMPLATE = ".*{keyWord}.*";
const SEARCH_ALL = ".*";
const MINIMUM_FROM_DATE = "1900-01-01";
const MAXIMUM_TO_DATE = "9999-12-31";
const TIME_DEFAULT = "T00:00:00";

/* ******** router準備 ******** */
const route = useRoute();

/* ******** ライフサイクルフック ******** */
onIonViewWillEnter(async function () {
  // ストレージオブジェクト生成
  const store = new Storage();
  storage = await store.create();

  const bank = String(route.params["bank"]);
  const accountHolder = String(route.params["accountHolder"]);
  const fromDate = String(route.params["fromDate"]);
  const toDate = String(route.params["toDate"]);

  searchBalance(bank, accountHolder, fromDate, toDate);
});

/* ******** メソッド ******** */
/* ----------------------------------
method:残高情報選択
detail:検索条件にあった残高情報を取得する
 ---------------------------------- */
const searchBalance = function (bank, accountHolder, fromDate, toDate) {
  createGraph(bank, accountHolder, fromDate, toDate);
};

/* ----------------------------------
method:グラフ作成。
detail:グラフに必用なデータを取得し、グラフを描画する。
 ---------------------------------- */
const createGraph = function (bank, accountHolder, fromDate, toDate) {
  // 検索条件の組み立て
  // 銀行
  let searchBank = checkEmptyValue(bank)
    ? SEARCH_ALL
    : SEARCH_TEMPLATE.replaceAll("{keyWord}", bank);
  // 口座名義人
  let searchAccountHolder = checkEmptyValue(accountHolder)
    ? SEARCH_ALL
    : SEARCH_TEMPLATE.replaceAll("{keyWord}", accountHolder); // 口座名義人
  // 対象日(FROM)
  let searchFromDate = checkEmptyValue(fromDate) ? MINIMUM_FROM_DATE : fromDate;
  // 対象日(TO)
  let searchToDate = checkEmptyValue(toDate) ? MAXIMUM_TO_DATE : toDate;

  // 口座情報のリストを生成
  let accountList = [];
  storage.forEach((value, key) => {
    let infoJson = JSON.parse(value);
    // データタイプがaccountInfoなら口座情報として読み込む
    if (infoJson.dataType === DATA_TYPE_FOR_ACCOUNT) {
      accountList.push({
        key: key,
        bank: infoJson.bank,
        accountHolder: infoJson.accountHolder,
      });
    }
  });

  // 日付別に残高を合算
  const recordDataMap = new Map();
  storage
    .forEach((value, key) => {
      let infoJson = JSON.parse(value);

      // accountKeyから口座情報を取得して銀行と口座名義人を取得
      let accountInfoBank = "";
      let accountInfoAccountHolder = "";
      accountList.forEach((account) => {
        if (account.key === infoJson.accountKey) {
          accountInfoBank = account.bank;
          accountInfoAccountHolder = account.accountHolder;
          return true;
        }
      });

      // データタイプがbalanceInfoなら残高情報として読み込む
      if (infoJson.dataType === DATA_TYPE_FOR_BALANCE) {
        let balanceData = {
          key: key,
          accountInfoBank: accountInfoBank,
          no: infoJson.no,
          recordDate: infoJson.recordDate,
          balance: infoJson.balance,
          bank: accountInfoBank,
          accountHolder: accountInfoAccountHolder,
        };
        // 表示対象の場合は日付別に合算する。
        if (
          checkBalanceInfoMatchCondition(
            balanceData,
            searchBank,
            searchAccountHolder,
            searchFromDate,
            searchToDate
          )
        ) {
          if (recordDataMap.has(balanceData.recordDate)) {
            let newBalance =
              Number(infoJson.balance) +
              Number(recordDataMap.get(balanceData.recordDate));
            recordDataMap.set(balanceData.recordDate, newBalance);
          } else {
            recordDataMap.set(balanceData.recordDate, infoJson.balance);
          }
        }
      }
    })
    .then(() => {
      let graphData = []; // 描画用のデータ
      recordDataMap.forEach((value, key) => {
        graphData.push({ x: key, y: value });
      });

      //データを時系列順に並べ替え
      graphData.sort(function (first, second) {
        const firstRecordDate = new Date(first.x);
        const secondRecordDate = new Date(second.x);
        if (firstRecordDate < secondRecordDate) {
          return -1;
        } else if (firstRecordDate > secondRecordDate) {
          return 1;
        } else {
          return 0;
        }
      });

      describeGraph(graphData);
    });
};

/* ----------------------------------
method:残高情報条件チェック
detail:残高情報が検索条件に合致する場合はTrueを返却する
 ---------------------------------- */
const checkBalanceInfoMatchCondition = function (
  balanceData,
  searchBank,
  searchAccountHolder,
  searchFromDate,
  searchToDate
) {
  // 銀行チェック
  const searchBankRegExp = new RegExp(searchBank);
  if (!searchBankRegExp.test(balanceData.bank)) {
    return false;
  }
  // 口座名義人チェック
  const searchAccountHolderRegExp = new RegExp(searchAccountHolder);
  if (!searchAccountHolderRegExp.test(balanceData.accountHolder)) {
    return false;
  }
  // 対象日(FROM)
  if (new Date(balanceData.recordDate) < new Date(searchFromDate)) {
    return false;
  }
  // 対象日(TO)
  if (new Date(balanceData.recordDate) > new Date(searchToDate)) {
    return false;
  }
  return true;
};

/* ----------------------------------
method:空文字・NULL・undefined判定
detail:空文字・NULL・undefinedの場合、trueを返す。
 ---------------------------------- */
const checkEmptyValue = function (targetValue) {
  if (targetValue === null || targetValue === "" || targetValue === undefined ||  targetValue === "undefined") {
    return true;
  }
  return false;
};

/* ----------------------------------
method:グラフ描画
detail:受け取った値のリストに対するグラフを描画する。
 ---------------------------------- */
const describeGraph = function (data) {
  // test
  new Chart("chart", {
    type: "line",
    data: {
      datasets: [
        {
          label: "BALANCE GRAPH",
          backgroundColor: "rgb(255, 99, 132)",
          borderColor: "rgb(255, 99, 132)",
          fill: false,
          data: data,
        },
      ],
    },
    options: {
      aspectRatio: 0.6,
      scales: {
        xAxes: [
          {
            type: "time",
            time: {
              unit: "day",
            },
          },
        ],
      },
    },
  });
};
</script>

<style scoped>
</style>
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?