src/components/LangTable.js
/**
* The LangTable contains the top 50 programming languages
* for a given quarter, e.g. 2017/Q2
* Features: Trend, Percentage & Change, History, different data sets
* PR/Push/Stars ...
* @author Fabian Beuke <mail@beuke.org>
* @license AGPL-3.0
*/
import { useState, useEffect } from "react";
import { BootstrapTable, TableHeaderColumn } from "react-bootstrap-table"
import NoLanguages from "common/NoLanguages"
import RenameLanguages from "common/RenameLanguages"
import _ from "lodash/fp"
export default function LangTable({store, hist, table}) {
const [state, setState] = useState({ data: []})
const [numberState, setNumberState] = useState(50)
const style = {
margin: "auto",
maxWidth: 810,
overflowX: "auto"
}
/**
* Filter data set by year and quarter
* @param {Object} data - GitHub api data set
* @param {number} year - Example 2017
* @param {number} quarter - Example 2
*/
function filterDate(data, year, quarter) {
return _.pipe(
_.filter({ year: year, quarter: quarter }),
_.map(_.pick(["name", "count"]))
)(data)
}
/**
* The GitHub API keeps track of languages such as
* Jupyter Notebook in which we are not interested,
* since these are no programming languages
* Filters non programming languages
* @param {Object} data - GitHub api data set
*/
function filterNonProgrammingLanguages(data) {
return _.reject((o) => _.includes(o.name)(NoLanguages))(data)
}
/**
* The GitHub API changes names (language renamings),
* therefore we keep track of them and always choose
* the latest name (replace old names) to keep consistency
* @param {Object} data - GitHub api data set
*/
function applyLanguageRenamings(data) {
const rename = (name) => {
const r = _.find((o) => _.includes(name, o.before))(RenameLanguages)
return r ? r.after : name
}
return _.map(_.update("name")(rename))(data)
}
/**
* Two up arrows: more than 3 ranks up or previoulsy unkown
* Two down arrorw: more than 3 ranks down
* One up arrow: 1-3 ranks up
* One down arrow: 1-3 ranks down
* Nothing: no change
* @param {Object} cell - Cell content of the table
* @param {Object} row - Row content of the table
*/
function trendFormatter(cell, _) {
const arrow = (n) => {
const angle = (dir) => `<i class='fas fa-angle-${dir}'></i>`
switch (true) {
case n === 0:
return ""
case n > 3:
return angle("double-up")
case n < -3:
return angle("double-down")
case n < 0:
return angle("down")
case n > 0:
return angle("up")
default:
// direct jump to top 50 and previously unkown
return angle("double-up")
}
}
return `${arrow(cell)}`
}
/**
* Lemma function filters dataset by name and returns first it finds
* @param {Object} data - GitHub api data set
* @param {string} name - Name to search for
* @returns {Object} Search result
*/
function findByName(data, name) {
return _.pipe(_.filter({ name: name }), _.first, _.omitBy(_.isNil))(data)
}
/**
* Calculate the trend difference in ranking over a one year period
* @param {Object} current - GitHub api data set current year
* @param {Object} last - GitHub api data set last year
* @returns {Object} Data set ++ trend diff
*/
function getTrend(current, last) {
return _.pipe(
_.map((c) =>
_.assign({
trend: findByName(last, c.name).id - c.id,
})(c)
),
_.take(numberState)
)(current)
}
/**
* Calculate the percental change in ranking over a one year period
* @param {Object} current - GitHub api data set current year
* @param {Object} last - GitHub api data set last year
* @returns {Object} Data set ++ change as number
*/
function getChange(current, last) {
return _.pipe(
_.map((c) =>
_.assign({
change: c.count - findByName(last, c.name).count,
})(c)
),
_.take(50)
)(current)
}
/**
* Composition of multiple functions, such as non programming
* language filter, language renaming and indexing to create
* a table from a raw data set for a given year and quarter
* @param {Object} data - GitHub api data set
* @param {number} year - e.g. 2017
* @param {number} quarter - e.g. 2
* @returns {Object} filtered and indexed data set (table)
*/
function createTable(date, year, quarter) {
const addSortId = _.map.convert({ cap: 0 })((o, i) =>
_.assign({ id: ++i })(o)
)
return _.pipe(
filterNonProgrammingLanguages,
applyLanguageRenamings,
addSortId,
percentageData
)(filterDate(date, year, quarter))
}
/**
* Sets ranking table state based on given props (api data)
* Gets called on componentDidMount and sets react state on prop change
*/
function mountTable() {
const data = store[0].data
const { year, quarter } = hist[0]
const dec = (i) => _.toString(--i)
const curYearRanking = createTable(data, year, quarter)
const lastYearRanking = createTable(data, dec(year), quarter)
const trendRanking = getTrend(curYearRanking, lastYearRanking)
const langRanking = getChange(trendRanking, lastYearRanking)
if (!_.isEqual(state.data, langRanking)) {
const [, dispatch] = table
dispatch({ type: "set", payload: langRanking })
setState({ data: langRanking })
}
}
/**
* Converts the absolute raw counts to percentage values
* A "count" is e.g. the number of Pull Requests per language
* @param {Object} data - GitHub api data set
* @returns {Object} Data set with percentage count
*/
function percentageData(data) {
const total = _.pipe(_.map("count"), _.map(Number), _.sum)(data)
return _.pipe(_.map(_.update("count")((d) => d / total)))(data)
}
/**
* Native react function, called on component mount and
* on every prop change
*/
useEffect(() => {
mountTable()
}, [hist, table , numberState])
/**
* Formatter that applies color, percentage and change from raw
* cell data, make it visual grokkable
* @param {Object} cell - Cell content of the table
* @param {Object} row - Row content of the table
* @returns {Object} Formatted cell content
*/
function percentFormatter(cell, row) {
const font = (i, color) =>
`<font size="1">(<font color="${color}">${i}%</font>)</font>`
const colorize = (i) =>
i >= 0 ? font("+" + i, "green") : font(i, "chrimson")
const percent = (i) => (i * 100).toFixed(3)
const countPercent = (percent(row.count)) + "%"
// NaN can happen in case of new first seen languages,
// hence we say 0% change
const normalize = (n) => (_.isNaN(n) ? 0.0 : n)
const offset = "\u00A0".repeat(6)
return (
offset +
(row.id > 30
? countPercent
: `${
countPercent +
(" " + (_.pipe(normalize, percent, colorize)(row.change)))
}`)
)
}
/**
* Provides empty default table when no data is available or data is
* still loading
* @returns {Object} html content for emtpy table
*/
function noDataAvailableYet() {
return (
<div className="emptyTable">
<h2>No data available for time period yet</h2>
</div>
)
}
/**
* Is called when the input range change, to update the state
*/
function handleInput(event) {
if(numberState<=0){
setNumberState(0);
}
setNumberState(event.target.value);
}
if (state.data.length <= 0) return noDataAvailableYet()
return (
<div style={style}>
<div className="inputBox">
<label>Number of languages: <b>{numberState}</b> </label>
<input onChange={handleInput} min="2" max="50" className="inputBox__input" type="range" value={numberState} ></input>
</div>
<BootstrapTable
condensed
striped
tableStyle={{ margin: "30px auto 30px auto", width: "100%" }}
data={state.data}
bordered={false}
wrapperClasses="table-responsive"
>
<TableHeaderColumn
width="100px"
dataAlign="center"
dataField="id"
isKey={true}
>
{"# Ranking"}
</TableHeaderColumn>
<TableHeaderColumn
width={"230px"}
dataAlign="center"
dataField="name"
>
{"Programming Language"}
</TableHeaderColumn>
<TableHeaderColumn
width={"230px"}
dataField="count"
dataAlign="center"
dataFormat={percentFormatter}
>
Percentage (YoY Change)
</TableHeaderColumn>
<TableHeaderColumn
width="100px"
dataAlign="center"
dataField="trend"
dataFormat={trendFormatter}
>
YoY Trend
</TableHeaderColumn>
</BootstrapTable>
</div>
)
}