Source

geoms.js

/**
 * @module
 * @description Module with available visualization functions (called `geoms`) to display the data.
 * The following table summarizes available geoms for different data types:
 * | Data type | Name | Link | Size mapping | Color mapping |
 * | --- | --- | --- | --- | --- |
 * | `number` | `funkyrect` | {@link module:geoms.funkyrect} | ✅ | ✅ |
 * | `number` | `circle` | {@link module:geoms.circle} | ✅ | ✅ |
 * | `number` | `bar` | {@link module:geoms.bar} | ✅ | ✅ |
 * | `number` | `rect` | {@link module:geoms.rect} | 🚫 | ✅ |
 * | `string` | `text` | {@link module:geoms.text} | 🚫 | ✅ |
 * | `number[]` | `pie` | {@link module:geoms.pie} | 🚫 | ✅ |
 * | `image` | `image` | {@link module:geoms.image} | 🚫 | 🚫 |
 *
 * Each geom is a function with the signature of {@link module:geoms~geom|geom}.
 */

import * as d3 from 'd3';

/**
 * @name geom
 * @constant
 * @function
 * @abstract
 * @description Abstract virtual function representing a geom.
 *
 * @param {number|number[]|string} value - data value to be visualized, of the above types
 * @param {number|string} colorValue - value to be used for color mapping, if applicable
 * @param {module:columns.Column} column - column object
 * @param {HeatmapOptions} O - heatmap options
 * @param {PositionArgs} P - position arguments
 * @returns {SVGElement} - SVG element representing the geom
 */

export const GEOMS = {
    /**
     * @memberof module:geoms
     * @see {@link module:geoms~geom|geom} for function signature
     * @description Text geom. Renders text string. Configured with `fontSize` and `align` options.
     *   Default fontSize is inherited from {@link HeatmapOptions}. Default align is `left`.
     *   Color is mapped from palette by text value, if palette is defined (see
     *   {@link module:palettes~CustomPalette|CustomPalette}).
     */
    text: (value, _, column, O, P) => {
        let fill = O.theme.textColor;
        if (column.palette && column.palette !== 'none') {
            fill = column.palette(value);
        }
        let align = 'start', x = 0;
        if (column.options.align === 'center' || column.options.align === 'middle') {
            align = 'middle';
            x = P.rowHeight / 2;
        }
        if (column.options.align === 'right' || column.options.align === 'end') {
            align = 'end';
            x = P.rowHeight - P.padding;
        }
        const el = d3.create('svg:text')
            .classed('fh-geom', true)
            .attr('dominant-baseline', 'middle')
            .attr('y', P.rowHeight / 2)
            .attr('x', x)
            .attr('text-anchor', align)
            .style('fill', fill)
            .text(value);
        if (O.fontSize) {
            el.attr('font-size', O.fontSize);
        }
        if (column.options.fontSize) {
            el.attr('font-size', column.options.fontSize);
        }
        return el;
    },

    /**
     * @memberof module:geoms
     * @see {@link module:geoms~geom|geom} for function signature
     * @description Bar geom. Renders a bar with width proportional to value. Maximum bar width is
     *   configured with `width` property ({@link module:columns~ColumnInfo|ColumnInfo}). If value
     *   is 0, minimal bar width is set from {@link PositionArgs} `minGeomSize`.
     */
    bar: (value, colorValue, column, O, P) => {
        const fill = column.palette(colorValue);
        value = column.scale(value);
        let width = value * column.width * P.geomSize;
        if (width === 0) {
            width = P.minGeomSize;
        }
        return d3.create('svg:rect')
            .classed('fh-geom', true)
            .attr('x', P.geomPaddingX)
            .attr('y', P.geomPadding)
            .attr('width', width.toFixed(2))
            .attr('height', P.geomSize)
            .style('stroke', O.theme.strokeColor)
            .style('stroke-width', 1)
            .style('fill', fill);
    },

    /**
     * @memberof module:geoms
     * @see {@link module:geoms~geom|geom} for function signature
     * @description Circle geom. Renders a circle with radius proportional to value. If value is 0,
     *   minimal circle radius is set from {@link PositionArgs} `minGeomSize`.
     */
    circle: (value, colorValue, column, O, P) => {
        const fill = column.palette(colorValue);
        value = column.scale(value);
        let radius = value * P.geomSize / 2;
        if (radius === 0) {
            radius = P.minGeomSize;
        }
        return d3.create('svg:circle')
            .classed('fh-geom', true)
            .style('stroke', O.theme.strokeColor)
            .style('stroke-width', 1)
            .style('fill', fill)
            .attr('cx', P.rowHeight / 2)
            .attr('cy', P.rowHeight / 2)
            .attr('r', radius.toFixed(2));
    },

    /**
     * @memberof module:geoms
     * @see {@link module:geoms~geom|geom} for function signature
     * @description Square geom. Renders a square of standard size, but color is mapped from
     *   palette.
     */
    rect: (value, colorValue, column, O, P) => {
        const fill = column.palette(colorValue);
        value = column.scale(value);
        return d3.create('svg:rect')
            .classed('fh-geom', true)
            .style('stroke', O.theme.strokeColor)
            .style('stroke-width', 1)
            .style('fill', fill)
            .attr('x', P.geomPaddingX)
            .attr('y', P.geomPadding)
            .attr('width', P.geomSize)
            .attr('height', P.geomSize);
    },

    /**
     * @memberof module:geoms
     * @see {@link module:geoms~geom|geom} for function signature
     * @description Funkyrect geom. Renders a circle that grows into a square with rounded corners.
     *   Value below {@link PositionArgs} `funkyMidpoint` is rendered as a circle, above as
     *   a square, with corner radius decreasing as value grows.
     */
    funkyrect: (value, colorValue, column, O, P) => {
        let scaled = column.scale(value);
        const fill = column.palette(colorValue);
        if (scaled < P.funkyMidpoint) {
            // transform value to a 0.0 .. 0.5 range
            value = column.scale.copy()
                .range([0, 0.5])
                .domain([column.min, column.min + column.range * P.funkyMidpoint])(value);
            let radius = (value * 0.9 + 0.1) * P.geomSize - P.geomPadding; // 0.5 for stroke
            if (radius <= 0) {
                radius = P.minGeomSize;
            }
            return d3.create('svg:circle')
                .classed('fh-geom', true)
                .style('stroke', O.theme.strokeColor)
                .style('stroke-width', 1)
                .style('fill', fill)
                .attr('cx', P.rowHeight / 2)
                .attr('cy', P.rowHeight / 2)
                .attr('r', radius.toFixed(2));
        }
        // transform value to a 0.5 .. 1.0 range
        value = column.scale
            .copy()
            .range([0.5, 1])
            .domain([column.min + column.range * P.funkyMidpoint, column.max])(value);
        const cornerSize = (0.9 - 0.8 * value) * P.geomSize;
        return d3.create('svg:rect')
            .classed('fh-geom', true)
            .style('stroke', O.theme.strokeColor)
            .style('stroke-width', 1)
            .style('fill', fill)
            .attr('x', P.geomPaddingX)
            .attr('y', P.geomPadding)
            .attr('width', P.geomSize)
            .attr('height', P.geomSize)
            .attr('rx', cornerSize.toFixed(2))
            .attr('ry', cornerSize.toFixed(2));
    },

    /**
     * @memberof module:geoms
     * @see {@link module:geoms~geom|geom} for function signature
     * @description Pie chart geom. Renders a pie chart with slices proportional to values.
     */
    pie: (value, _, column, O, P) => {
        let nonZero = 0;
        let nonZeroIdx = 0;
        value.forEach((x, i) => {
            if (x > 0) {
                nonZero += 1;
                nonZeroIdx = i;
            }
        });
        if (nonZero === 1) {
            const fill = column.palette(nonZeroIdx);
            return d3.create('svg:circle')
                .classed('fh-geom', true)
                .style('stroke', O.theme.strokeColor)
                .style('stroke-width', 1)
                .style('fill', fill)
                .attr('cx', P.rowHeight / 2)
                .attr('cy', P.rowHeight / 2)
                .attr('r', P.geomSize / 2);
        }

        const arcs = d3.pie().sortValues(null)(value);
        const g = d3.create('svg:g');
        g.classed('fh-geom', true);
        g.selectAll('arcs')
            .data(arcs)
            .enter()
            .append('path')
                .attr('d', d3.arc().innerRadius(0).outerRadius(P.geomSize / 2))
                .attr('fill', (_, i) => {
                    return column.palette(i);
                })
                .style('stroke', O.theme.strokeColor)
                .style('stroke-width', 1)
                .attr('transform', `translate(${P.rowHeight / 2}, ${P.rowHeight / 2})`);
        return g;
    },

    /**
     * @memberof module:geoms
     * @see {@link module:geoms~geom|geom} for function signature
     * @description Image geom. Renders an image with standard height and width specified in column
     *   options (see {@link module:columns~ColumnInfo|ColumnInfo} `width`).
     */
    image: function(value, _, column, O, P) {
        return d3.create('svg:image')
            .classed('fh-geom', true)
            .attr('y', P.geomPadding)
            .attr('href', value)
            .attr('height', P.geomSize)
            .attr('width', column.width)
            .attr('preserveAspectRatio', 'xMidYMid');
    }
};