| @ブログ

#100DaysToOffload 7 月のふり返り

7 月 10 日に #100DaysToOffload に挑戦する宣言をしてからしばらくは調子よく記事を書けていたが、後半にだれてしまった。 11 記事以上書かなければならないところ、 9 記事しか書けなかった。

宣言後に最初に書いた記事は、昨年末のセールで買った新しい Kindle Paperwhite についての記事。ホーム画面がリッチになって目移りしてしまい本が読めなくなったというチラウラ記事だが、 Twitter で nagayama さんからリプライをもらって設定で回避できることを知った。情報を発信するところに情報が集まってくる良い例だと思った。

一番反響があったのは、エンジニアの求人を出している会社の顔ぶれが変わってきているという記事。一昔前は受託開発をやっている会社かウェブサービスをやっている会社が雑にエンジニア( HOW )を募集していて、何( WHAT )をやるかはエンジニアを採用したあとに顧客や状況に応じて考えるという感じだった。いまのエンジニア求人は何か解決したい課題があって起業された会社( WHAT )がエンジニア( HOW )を採用しようとしている感じ。 WHAT と HOW の主客が逆転しつつあるのでは、ということをまとめて書いた。

Numbers に関しての記事は、 Vim 、 Day One 、 Notion 、 WorkFlowy 、 Journler 、Yojimbo 、 Evernote などこれまでいろんなドキュメントエディター・オーガナイザーを使ってきて実現できなかったことが Numbers を使うとできるということをまとめようとして書いたが若干不完全燃焼感があった。例として出した登山の計画がニッチすぎたかもしれない。一般的な旅行の予定を Numbers で立てる、みたいな感じで紹介すればよかったかも。そういや Numbers のテンプレートに旅行計画ありますね。 Mac ユーザーで表計算ソフト使ってる人はわざわざ MS Office 入れて Excel 使ってるかもしれないけど Numbers はもっと評価されてよい。

Z6 の良さについては、オペミスで吹っ飛ばした過去写真の整理をしていて改めてその良さを伝えたいと思い立ち書いてみた。 NIKON は株価が低迷しているが、 Z は本当によいのでおすすめ。撮った写真をすぐに Bluetooth でスマートフォンに転送しつつ位置情報も付与してくれる機能が特に気に入っている。ちなみに NIKON は公式には Z 6Z 7 という Z と 数字の間にスペースを入れた名称にしているが、ググラビリティが最悪なので正式に製品名を Z6Z7 (スペースなし)にした方がよいと思う。

家の近所の長垂海浜公園の駐車場についての記事は本当にしょうもない記事という感じがあふれているが、たまにこういうのを書きたくなる。近所に住んでいる人間でないとわからない情報をインターネットに載せて社会貢献したいと思い、わざわざ夜に散歩してたときに写真を撮って回って記事化した。この手の記事は短期的にはアクセスが伸びないが後からじわじわと伸びてくる予感がある。 Garage Band でのアナログレコードの録音方法の記事 は 10 年以上前の記事だけどいまでもコンスタントにアクセスがある。

ブログは書かなくなると更新が滞ってしまうので、あまりクオリティの高いものを書こうと思わず、もっとライトな感じで更新していきたい。

なお、この記事を含めて 2020 年の記事は 40 記事なので、残り 5 ヶ月で 60 記事書かないといけない。一月あたり 12 記事は結構大変だ。

| @写真

昨年末に今年撮った写真アドベントカレンダーに参加したときに書いた写真で振り返る 2019 年 という記事があるが、今年の 3 月に事故って S3 バケットの画像を吹っ飛ばしてしまったので記憶を辿りながらちまちま画像を上げ直していた。改めて Z6 で撮った写真は良いなと思ってたところで NIKON から新しいフルサイズのミラーレスカメラが発表された。

重さはほぼ同じらしい。 USB-C による給電(バッテリーすっからかんの状態で USB-C ケーブルで給電して撮影する)ことが可能になったみたい。 Z6 は電源を入れた状態では充電できないのだが、バッテリーがない状態でも外部から電源を取りながら撮影することができるのでスタジオカメラマンとかウェブカメラとして使う場合には便利なのかもしれない。また、 Z6 では XQD カードか CFExpress カードでないと使えず、しかもシングルスロットなのだが、 Z5 だと SD カードでデュアルスロットになっているようだ。

kakaku.com で調べると Z6 の 24-70 f/4 レンズキットは Z5 が Z6 の上位機種ではなかったためか Z5 発表後に値上がりしてて 27 万円くらいになっている。一方で FTZ という F マウントレンズが Z マウントでも使えるようにするマウントアダプター付きの 24-70 f/4 レンズキットの方が安くなってて 24 万円弱で買えるっぽい。 Z5 の 24-50 f/4-6.3 レンズキットはヨドバシで 22 万円ちょいなのでこれだったら自分は Z6 の 24-70 f/4 FTZ 付きキットの方を激しくおすすめする。連写性能やボタン、サブディスプレイが省かれている Z5 よりも Z6 の方がよい。メディアに SD カードが使えないのはデメリットに見えるかもしれないが( XQD は高いし別途カードリーダーが必要)、高画質で高速に書き込むためのコストだと思えばよい。

Z6 で撮った写真を以下に貼っておきます。

ワイキキの夕日

ワイキキの夕暮れ

ノースショアの岩礁

長垂海岸の夕暮れ

炭火

低温調理鶏のネギ油がけ

ドライフラワー

瑞梅寺川の桜

| @Mac/iPhone

Numbers.app

公共交通機関を使って登山に行くときは結構綿密に登山計画を立てる。登山口まで行けるバスはコミュニティバスのようなものが多く本数が少ないため、乗り継ぎや行程の時間管理に失敗すると登山できなくなったり帰れなくなったりする。特に長い距離の縦走を日帰りでやろうとすると時間の管理がシビアになる。コミュニティバスの最終時刻は精々 18 時くらいなので、その時間までに確実に下山しないといけない。もし下山できなかった場合はタクシーを呼んで大金を払って帰るか山で野垂れ死ぬしかない。なので準備が大事だ。

YAMAP やヤマレコに登山計画を立てる機能はあるが、あくまでそれは登山中の行程管理であって、行き帰りの公共交通機関の情報を含めた行程ではない。何時に家を出ると乗換駅には何時頃着いてバスはどれに乗れば良いか、バスの乗り換えはどこですればよいか、といった情報は登山計画には書けず自分で管理するしかない。

他にも、バスの時間を一本遅らせたときに後ろの行程にどのくらい影響が出るかを確認したいが、登山の行程と交通機関の行程が分離されていると影響を把握しづらい(手動で後ろの行程の時間をずらしていく必要がある)し、バスの時刻表や地図を埋め込んでおきたいが、画像の貼り付けやリンクには対応していない。

登山では(登山に限らず旅行などでも)プランA とプラン B を考えて、その日の体調や天候に応じて行程を変更するということがあり得ると思う。複数の計画を並列で眺めて比較検討したりするのも登山計画系のサービスではできない。

旅の計画とはつまるところタイムテーブルの管理であり、それはエクセル的なものが使いやすい。スタート時間を 5 分遅らせると後ろが何分遅れるかが簡単にわかる。この計画は何時間かかるのか、というのも勝手に計算してくれる。エクセル的なものであればリンクを埋め込んだり画像を貼り付けたりもできる。

ただ、 Microsoft Excel や Google Spreadsheet の弱点として、一つのシート(画面)に表示できるのは一つの表までだ。一つの画面で複数の表を並べて情報を整理したりできない。画像やリンクを埋め込むことはできるが、あくまで表情のどこかに置くという感じで使い勝手が悪い。セルの中に文字列が隠れてしまったりする。

Numbers は一つのシート(画面)に複数の表を表示できる。これにより関連する複数のデータを並べて情報を整理することができる。それぞれの表はグリグリ動かすこともできる。画像やリンクは表の一部としてではなく、独立したオブジェクトとしてシートの中に埋め込むことができる。こんな感じ。

Numbers

例えるなら表計算機能付きのスクラップブックといった感じだ。たいていのデジタルデータを取り込めて自由に配置でき、コメントを書いたりデータを表に集計して絞り込みしたりグラフ化したりもできる。

いろんなデータを取り込めると言えば Notion が思い浮かんだので同じようなことを Notion でやってみようとした。見た目はおしゃれだし画像やリンクの埋め込みは Notion の方に分があるが、表は作れるものの時間の計算ができない。行程管理において時間の計算ができないのは致命的だ。

Notion

SIer がドキュメント管理に Excel を使うのを嘲笑する風潮があるが(自分もかつて Excel で画面仕様書を作らされていて死ぬほど嫌だった)、何でも Excel で書くのは一理あるのかもしれない。

ただ、上に書いているように Excel には欠点があるので Mac が使えるなら断然 Numbers の方がよい。 Excel のようなマクロはないので高度な処理には向かないが、個人が普通に使う計算はできる。

Numbers のような機能を持ち、チームで共同編集もできる SaaS が出てくると市場を席巻できる気がする。 Miro などがそれに近いかもしれないが、ドローやダイアグラムに特化していてちょっとした計算や条件に応じたデータの絞り込みなどはできない。

話をもとに戻すと、個人が旅行計画のような図や写真、表を一元管理して情報を整理するような用途には Numbers が最適です

| @写真

Flickr の Pro プランの有効期限が今月末で切れるので念のため全データを DL しておいた。自分は 2007 年に車上荒らしにあって PowerBook 17" を盗まれてしまい、そこに保存しておいたそれまでの人生のデジタル写真もすべて失ってしまった。一部は Flickr にアップロードしておいたので、それらを Mac 上に取り込むのが目的。 Flickr はちゃんとしていて、データの DL リクエストをすると一日くらいで画像だけでなく文章はコメント含めて全部 DL できるようになってる。 GDPR 便利。

何枚かの写真はアップロード時に縮小されてしまっていて、 5K のディスプレイで見るとかなりちっちゃい。試みに image upscale などでググってみると以下の Web サービスが見つかった。

最大 4 倍までアップスケールできる。利用にはアカウント登録が必要だった。無料っぽいので試してみた。アップロードしたのは以下の写真。この写真は 800×600 の解像度でしか落とせなかった。

VW Golf 2 GTI

それがこうなった。ぱっと見綺麗。

VW Golf 2 GTI Upscaled

ただし拡大してみると欠点がないわけではなかった。こんな感じ。

Deep Image Drawbacks

暗い部分、光が反射している部分は綺麗にアップスケールできないみたいだった。機械学習モデルが進化するとこの辺も綺麗にアップスケールできるようになるのかな。

変換の方法はファイルを都度アップロードする方法のほかに、 Google Drive にファイルを置いてディレクトリ指定で一括変換したり、REST API も用意されていて、 curl や自分でプログラムを書いて変換させることも可能っぽい。なかなか便利そう。

なおこのサービスは無料で使えるのは 5 枚までで、それ以上はサブスクリプションか都度払いでお金がかかるシステムのようだ。

Deep Image Pricing

Flickr から落とした低解像度の写真が何枚かあるので都度課金かサブスクリプションの単月契約で利用してみようかなと思ってる。

| @ブログ

75 件ほどあった tech.portalshit.net の記事を取り込んだ。実家に住んでいた 10 年前に始めた技術ブログで、最初は Rails 製の Mephisto 、その次に Jekyll で構築した。まだ GitHub Pages の仕組みが存在する前で、自前で用意したさくら VPS に git push すると自動でビルドして記事が公開されるような仕組みを作ったりしてた。

職業プログラマーになろうとしてもがいてた頃にやってたブログで、いま読み返すと「頑張ってたんだな」感があっていなたい記事が多い。

だいぶ放置していて、いまは S3 で静的サイトとして公開していたのでそのまま放置でもよかったが、 10 年前と違って何でも一カ所にまとめて書いておきたいという気持ちが強くなって取り込むことにした。ブログはトピックを混ぜずに一つのトピックにフォーカスした方がよいと 10 年前は考えていたのだけど、最近の世の中のブログ記事の読まれ方は変わってきていて、一人の人のブログをフィードリーダーに登録して読むというより、 SNS をだら見していて流れてきた記事を適当に消費するというスタイルに変わってきているので、一つのブログに一つのテーマという書き分けは不要になったと感じる。

tech.portalshit.net を取り込んだおかげで Archive ページのグラフに占める技術記事の割合が増えた。

Tech category Bar extension

ちなみに取り込みは以下のようなコードを書いて SQL の INSERT 文に変換した。

require 'yaml'
require 'pathname'

files = Dir.glob(File.join(__dir__, '_posts', '*.markdown'))
files.each do |file|
  content = File.read(file)
  _, header, body = content.split('---')
  header_yml = YAML.load(header)
  title = header_yml['title']
  tags = header_yml['category']
  tags = tags.is_a?(Array) ? tags.map(&:downcase) : [tags&.downcase]
  slug = Pathname.new(file).basename.to_s.sub(/\d{4}\-\d{2}\-\d{2}\-(.+?)\.markdown/, '\1').gsub('_', '-')
  body = body.strip.gsub(/\n/, "\\n").gsub('\'', '\'\'')
  created_at = File.birthtime(file).to_s.sub(' +0900', '')
  updated_at = File.mtime(file).to_s.sub(' +0900', '')
  puts <<~EOS
    INSERT INTO entries(title, user_id, category_id, slug, markup, type, draft, body, frozen_tag_list, created_at, updated_at) VALUES('#{title}', 1, 6, '#{slug}', 'redcarpet', 'Post', 0, '#{body}', '#{tags.join(',')}', '#{created_at}', '#{updated_at}');
  EOS
end

| @技術/プログラミング

Chart

Rechars という React のチャートライブラリを利用して、 Archive ページにカテゴリーごとに記事を集計してグラフ化する機能を作った。

グラフの Bar にカーソルを載せると Tooltip が表示されて、具体的な件数がわかる。

Chart Tooltip

カテゴリごとに表示・非表示を切り替えることも可能。グラフ下のカテゴリー名( Legend )をクリックして切り替えられる。

Chart Show-Hide Toggle

ただし残念なことに Bar を非表示にしたときに Legend の表示を変化させるのが難しくてできていない。

仕事で使ってる Looker とか Redash であれば Legend をクリックして表示・非表示を切り替えることができ、それに連動して Legend の色をトーンダウンさせたりする機能が付属しているが、利用した Recharts にはその機能がなかった。 Bar の表示・非表示切り替えも標準サポートされていなかったので、 GitHub の Issue の情報を頼りに無理矢理実装した。

コードはこんな感じ。結構汚い Hack で、 Bar の表示・非表示を、表示用のキー文字列に空白を追加するかしないかで切り替えている。

  // クリックされたアイテムが `this.state.disabled` 配列の中にすでに存在していれば除外し、
  // 存在してなければ追加する
  selectBar(event) {
    let dataKey = event.dataKey.trim()
    if (this.state.disabled.includes(dataKey)) {
      this.setState({ disabled: this.state.disabled.filter(item => item !== dataKey) })
    } else {
      this.setState({ disabled: this.state.disabled.concat([dataKey]) })
    }
  }

  render() {
    return (
      <ResponsiveContainer height={500}>
        <BarChart
          data={this.state.data}
          margin={{
            top: 20, right: 20, left: 0, bottom: 20,
          }}
          style={{ fontSize: '14px' }}
        >
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="year" />
          <YAxis />
          <Tooltip labelStyle={{ color: '#000', fontWeight: 'bold' }} itemStyle={{ margin: '0 2px 0 4px', padding: '0' }} />
          {// Legend クリック時のコールバックに `this.selectBar` を指定する }
          <Legend onClick={this.selectBar} />
          {/*
            `this.state.categories` 配列と `this.state.disabled` 配列の内容を比較し、
            `this.state.disabled` に追加済のカテゴリーは dataKey に空白を追加することで非表示に
          */}
          {this.state.categories.map((category, index) => {
            let dataKey = this.state.disabled.includes(category) ? category + " " : category
            let color = this.colors[index % this.colors.length]
            return(<Bar key={index} dataKey={dataKey} stackId="a" fill={color} />)
          })}
        </BarChart>
      </ResponsiveContainer>
    );
  }

| @技術/プログラミング

react-select-before-after.jpg

一個前の記事のキャプチャにあるように、従来テキストリンクだった年とカテゴリーの選択をセレクトボックスにした。 git log -S したところ去年( 2019 年)の 11 月頃に変更を行ったみたいだ。もうそろそろ 2020 年になろうとしていて、 2005 年からやっていて年の数が 16 個になろうとしていてさすがに多すぎると思ったので整理のためにセレクトボックス化してコンパクトにした。

利用したのは React Select というパッケージで、色々カスタマイズできるみたいだけど面倒だったので素のまま使ってる。

Archives ページのリファクタリング のときのように、子コンポーネントのイベントをトリガーに親コンポーネントの setState() を呼び出すような作りになっている。作るときは結構難儀したけどおかげでスマートフォンで見たときも年やカテゴリーだけでファーストビューが埋まるということがなくなった。

react-select-smartphone-view.jpg

現在のコードをまるっと貼り付けるとこんな感じ↓。

子コンポーネント(年のセレクトボックス)

import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import Select from 'react-select'
import PropTypes from 'prop-types'
import { withRouter } from 'react-router-dom'

import history from './history'

class YearSelect extends Component {
  constructor(props) {
    super(props)
    this.state = {
      data: [],
      selectedOption: null
    }
    this.handleChange = this.handleChange.bind(this)
  }

  static propTypes = {
    match: PropTypes.object.isRequired,
    location: PropTypes.object.isRequired,
    history: PropTypes.object.isRequired
  }

  async loadYearSelectFromServer() {
    const request = await fetch('/archives/years.json')
    const response = await request.json()
    this.setState({ data: response })
  }

  componentDidMount() {
    this.loadYearSelectFromServer()
  }

  handleChange(selectedOption) {
    const year = selectedOption ? selectedOption.value : null
    if (year) {
      this.props.history.push(`/archives/${year}`)
    } else {
      this.props.history.push("/archives")
    }
    this.setState(
      { selectedOption },
      () => { this.props.update(year) }
    )
  }

  render() {
    const options = this.state.data.map(year => {
      return { value: year, label: year }
    })
    return (
      <div className="year-list">
        <Select
          value={this.state.selectedOption}
          onChange={this.handleChange}
          options={options}
          placeholder="Year"
          isClearable
        />
      </div>
    )
  }
}

const YearList = withRouter(YearSelect)

export default YearList

親コンポーネント( App.js )

import React, { Component } from 'react'
import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom'

import YearList from './YearList'
import CategoryList from './CategoryList'
import Archives from './Archives'

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      category: null,
      year: null,
      length: 0
    }
    this.updateCategory = this.updateCategory.bind(this)
    this.updateYear = this.updateYear.bind(this)
    this.setLength = this.setLength.bind(this)
  }

  updateCategory(category) {
    this.setState({ category })
  }

  updateYear(year) {
    this.setState({ year })
  }

  setLength(length) {
    this.setState({ length })
  }

  render() {
    return(
      <Router history={history}>
        <div className="archive-filter">
          <YearList update={this.updateYear} />
          <CategoryList update={this.updateCategory} activeCategory={this.state.category} />
          <div className="entry-length"><p>{this.state.length} entries</p></div>
        </div>
        <Switch>
          <Route
            exact path="/archives"
            render={(props) =>
              <Archives
                category={this.state.category}
                setLength={this.setLength}
                {...props}
              />
            }
          />
          <Route
            path="/archives/:year(\d{4})"
            render={(props) =>
              <Archives
                category={this.state.category}
                setLength={this.setLength}
                year={this.state.year}
                {...props}
              />
            }
          />
        </Switch>
      </Router>
    )
  }
}

export default App