#include <bits/stdc++.h>
using namespace std;
using ll = long long; using ull = unsigned long long;
#define int ll
using vb = vector<bool>; using vvb = vector<vb>;
using vi = vector<int>; using vvi = vector<vi>;
using vl = vector<ll>; using vvl = vector<vl>;
template<class T> using V = vector<T>;
template<class T> using VV = vector<V<T>>;
#define fi first
#define se second
#define maxs(x,y) (x=max(x,y))
#define mins(x,y) (x=min(x,y))
using pii = pair<int,int>; using pll = pair<ll,ll>;
#define FOR(i,a,b) for(ll i = (a); i < (ll)(b); ++i)
#define REP(i,n) FOR(i,0,n)
#define RFOR(i,a,b) for(ll i = (ll)(b)-1;i >= a;--i)
#define RREP(i,n) RFOR(i,0,n)
#define ALL(obj) (obj).begin(), (obj).end()
#define rALL(obj) (obj).rbegin(), (obj).rend()
#define eb(val) emplace_back(val)
const double PI = acos(-1), EPS = 1e-10;
const ll MOD = 1e9+7;
template<class T> istream& operator>>(istream &is,vector<T> &st){
  for(size_t i=0;i<st.size();++i) is >> st[i];
  return is;
}
template<class T> istream& operator>>(istream &is,vector<vector<T>> &st){
  for(size_t i=0;i<st.size();++i) is >> st[i];
  return is;
}
template<class T> ostream& operator<<(ostream &os, const vector<T> &st){
  for(size_t i=0;i<st.size();++i){
    if(i==st.size()-1) os << st[i];
    else os << st[i] << " ";
  }
  return os;
}
template<class T> ostream& operator<<(ostream &os, const vector<vector<T>> &st){
  for(size_t i=0;i<st.size();++i){
    os << st[i];
    if(i!=st.size()-1) os << endl;
  }
  return os;
}
struct union_find{//rank,経路圧縮で高速化
  vector<long long> data;//そのノードがrootであれば-（集合のサイズ）を表し、そうでなければ親ノードidを示す。
  union_find(long long size):data(size,-1){}//コンストラクタ,size個のノードを作成し、-1で初期化
  inline long long root(long long a){//根を探す,経路圧縮
    return data[a] < 0 ? a : data[a]=root(data[a]);
  }
  inline bool find(long long a,long long b){//a,bが同じ集合に属するか調べる
    return root(a) == root(b);
  }
  inline bool unite(long long a,long long b){//小さい方の木の根に大きい方の根を上書きする
    a = root(a),b = root(b);
    if(a==b) return false;//根が同じなら終了,uniteが失敗したことを出力
    if(data[a]<data[b]) swap(a,b); //集合サイズの大きい方の根に低い方の根をつける
    data[b] += data[a];//集合サイズを統合
    data[a] = b;//yの親にxをつける
    return true;//unite成功を出力
  }
  inline long long size(long long a){//aが属する集合のサイズを出力
    return -data[root(a)];
  }
};
signed main(){
  cin.tie(0);
  ios::sync_with_stdio(false);
  int n;
  cin >> n;
  vi x(n),y(n);
  REP(i,n){
    cin >> x[i] >> y[i];
  }
  union_find t(n);
  map<int,int> mx,my;
  REP(i,n){
    if(mx.find(x[i])==mx.end()) mx[x[i]] = i;
    else t.unite(mx[x[i]],i);
    if(my.find(y[i])==my.end()) my[y[i]] = i;
    else t.unite(my[y[i]],i);
  }
  // REP(i,n){
  //   cout << t.root(i) << endl;
  // }
  V<set<int>> ctx(n),cty(n);
  REP(i,n){
    ctx[t.root(i)].insert(x[i]);
    cty[t.root(i)].insert(y[i]);
  }
  set<int> jud;
  int ans = 0;
  REP(i,n){
    if(jud.find(t.root(i))==jud.end()){
      jud.insert(t.root(i));
      ans += ctx[t.root(i)].size()*cty[t.root(i)].size()-t.size(i);
    }
  }
  cout << ans << endl;
  // cout << jud.size() << endl;
  // vi ax,ay;
  // int last = -1;
  // bool c = false;
  // REP(i,n){
  //   if(last==x[i]){
  //     if(!c) ax.eb(1);
  //     if(c) ax.back()++;
  //     c = true;
  //   }else c = false;
  //   last = x[i];
  // }
  // last = -1;
  // c = false;
  // REP(i,n){
  //   if(last==y[i]){
  //     if(!c) ay.eb(1);
  //     if(c) ay.back()++;
  //     c = true;
  //   }else c = false;
  //   last = y[i];
  // }
  // ll sumx = accumulate(ALL(x),0LL);
  // ll sumy = accumulate(ALL(y),0LL);
  // ll ans = 0;
  // REP(i,ay.size()){
  //   ans += sumx * ay[i];
  // }
  // REP(i,ax.size()){
  //   ans += sumy * ax[i];
  // }
  // cout << ans << endl;
}


Measured memory_bytes: 22912