viz-lib/src/visualizations/cohort/Cornelius.tsx (184 lines of code) (raw):

/*! * React port of Cornelius library (based on v0.1 released under the MIT license) * Original library: http://restorando.github.io/cornelius */ import { isNil, isFinite, map, extend, min, max } from "lodash"; import moment from "moment"; import chroma from "chroma-js"; import React, { useMemo } from "react"; import Tooltip from "antd/lib/tooltip"; import { createNumberFormatter, formatSimpleTemplate } from "@/lib/value-format"; import chooseTextColorForBackground from "@/lib/chooseTextColorForBackground"; import "./cornelius.less"; const momentInterval = { daily: "days", weekly: "weeks", monthly: "months", yearly: "years", }; const timeLabelFormats = { daily: "MMMM D, YYYY", weekly: "[Week of] MMM D, YYYY", monthly: "MMMM YYYY", yearly: "YYYY", }; const defaultOptions = { initialDate: null, timeInterval: "monthly", noValuePlaceholder: "-", rawNumberOnHover: true, displayAbsoluteValues: false, initialIntervalNumber: 1, maxColumns: Infinity, title: null, timeColumnTitle: "Time", peopleColumnTitle: "People", stageColumnTitle: "{{ @ }}", numberFormat: "0,0[.]00", percentFormat: "0.00%", timeLabelFormat: timeLabelFormats.monthly, colors: { min: "#ffffff", max: "#041d66", steps: 7, }, }; function prepareOptions(options: any) { options = extend({}, defaultOptions, options, { initialDate: moment(options.initialDate), colors: extend({}, defaultOptions.colors, options.colors), }); return extend(options, { // @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message timeLabelFormat: timeLabelFormats[options.timeInterval], formatNumber: createNumberFormatter(options.numberFormat), formatPercent: createNumberFormatter(options.percentFormat), getColorForValue: chroma .scale([options.colors.min, options.colors.max]) .mode("hsl") .domain([0, 100]) .classes(options.colors.steps), }); } function isLightColor(backgroundColor: any) { backgroundColor = chroma(backgroundColor); const white = "#ffffff"; const black = "#000000"; return chroma.contrast(backgroundColor, white) < chroma.contrast(backgroundColor, black); } function formatStageTitle(options: any, index: any) { return formatSimpleTemplate(options.stageColumnTitle, { "@": options.initialIntervalNumber - 1 + index }); } function formatTimeLabel(options: any, offset: any) { // @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message const interval = momentInterval[options.timeInterval]; return options.initialDate .clone() .add(offset, interval) .format(options.timeLabelFormat); } function CorneliusHeader({ options, maxRowLength }: any) { // eslint-disable-line react/prop-types const cells = []; for (let i = 1; i < maxRowLength; i += 1) { cells.push( <th key={`col${i}`} className="cornelius-stage"> {formatStageTitle(options, i)} </th> ); } return ( <tr> <th className="cornelius-time">{options.timeColumnTitle}</th> <th className="cornelius-people">{options.peopleColumnTitle}</th> {cells} </tr> ); } function CorneliusRow({ options, data, index, maxRowLength }: any) { // eslint-disable-line react/prop-types const baseValue = data[0] || 0; const cells = []; for (let i = 1; i < maxRowLength; i += 1) { const value = data[i]; const percentageValue = isFinite(value / baseValue) ? (value / baseValue) * 100 : null; const cellProps = { key: `col${i}` }; if (isNil(percentageValue)) { // @ts-expect-error ts-migrate(2339) FIXME: Property 'className' does not exist on type '{ key... Remove this comment to see the full error message cellProps.className = "cornelius-empty"; // @ts-expect-error ts-migrate(2339) FIXME: Property 'children' does not exist on type '{ key:... Remove this comment to see the full error message cellProps.children = options.noValuePlaceholder; } else { // @ts-expect-error ts-migrate(2339) FIXME: Property 'className' does not exist on type '{ key... Remove this comment to see the full error message cellProps.className = options.displayAbsoluteValues ? "cornelius-absolute" : "cornelius-percentage"; // @ts-expect-error ts-migrate(2339) FIXME: Property 'children' does not exist on type '{ key:... Remove this comment to see the full error message cellProps.children = options.displayAbsoluteValues ? options.formatNumber(value) : options.formatPercent(percentageValue); const backgroundColor = options.getColorForValue(percentageValue); // @ts-expect-error ts-migrate(2339) FIXME: Property 'style' does not exist on type '{ key: st... Remove this comment to see the full error message cellProps.style = { backgroundColor, color: chooseTextColorForBackground(backgroundColor), }; // @ts-expect-error ts-migrate(2339) FIXME: Property 'style' does not exist on type '{ key: st... Remove this comment to see the full error message if (isLightColor(cellProps.style.color)) { // @ts-expect-error ts-migrate(2339) FIXME: Property 'className' does not exist on type '{ key... Remove this comment to see the full error message cellProps.className += " cornelius-white-text"; } if (options.rawNumberOnHover && !options.displayAbsoluteValues) { // @ts-expect-error ts-migrate(2339) FIXME: Property 'children' does not exist on type '{ key:... Remove this comment to see the full error message cellProps.children = ( <Tooltip title={options.formatNumber(value)} mouseEnterDelay={0} mouseLeaveDelay={0}> {/* @ts-expect-error ts-migrate(2339) FIXME: Property 'children' does not exist on type '{ key:... Remove this comment to see the full error message */} <div>{cellProps.children}</div> </Tooltip> ); } } cells.push(<td {...cellProps} />); } return ( <tr> <td className="cornelius-label">{formatTimeLabel(options, index)}</td> <td className="cornelius-people">{options.formatNumber(baseValue)}</td> {cells} </tr> ); } type OwnCorneliusProps = { data?: number[][]; options?: { initialDate: any; // TODO: PropTypes.instanceOf(Date) timeInterval?: "daily" | "weekly" | "monthly" | "yearly"; noValuePlaceholder?: string; rawNumberOnHover?: boolean; displayAbsoluteValues?: boolean; initialIntervalNumber?: number; maxColumns?: number; title?: string; timeColumnTitle?: string; peopleColumnTitle?: string; stageColumnTitle?: string; numberFormat?: string; percentFormat?: string; timeLabelFormat?: string; colors?: { min?: string; max?: string; steps?: number; }; }; }; type CorneliusProps = OwnCorneliusProps & typeof Cornelius.defaultProps; export default function Cornelius({ data, options }: CorneliusProps) { options = useMemo(() => prepareOptions(options), [options]); const maxRowLength = useMemo( () => min([ // @ts-expect-error ts-migrate(2339) FIXME: Property 'length' does not exist on type 'number'. max(map(data, d => d.length)) || 0, // @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'. options.maxColumns + 1, // each row includes totals, but `maxColumns` is only for stage columns ]), [data, options.maxColumns] ); if (data.length === 0) { return null; } return ( <div className="cornelius-container"> {options.title && <div className="cornelius-title">{options.title}</div>} <table className="cornelius-table"> <thead> <CorneliusHeader options={options} maxRowLength={maxRowLength} /> </thead> <tbody> {map(data, (row, index) => ( <CorneliusRow key={`row${index}`} options={options} data={row} index={index} maxRowLength={maxRowLength} /> ))} </tbody> </table> </div> ); } Cornelius.defaultProps = { data: [], options: {}, };