Source: muze-legend/src/legend/gradient-helper.js

import { makeElement, applyStyle } from 'muze-utils';
import { ContinousAxis } from '@chartshq/muze-axis';
import { BOTTOM, RIGHT } from '../enums/constants';
import { ALIGN } from './defaults';
import '../styles.scss';

/**
 *
 *
 * @param {*} data
 *
 */
export const getGradientDomain = (data) => {
    if (typeof data[0].value === 'number') {
        return data.reduce((accumulator, currentValue) =>
            [Math.min(currentValue.value, accumulator[0]), Math.max(currentValue.value, accumulator[1])],
                [Number.MAX_VALUE, Number.MIN_VALUE]);
    }
    return data.map(e => e.value);
};

/**
 *
 *
 * @param {*} container
 * @param {*} data
 * @param {*} domain
 *
 */
export const makeLinearGradient = (container, data, domain) => {
    const defs = makeElement(container, 'defs', [1]);
    const linearGradient = makeElement(defs, 'linearGradient', [1])
                .attr('id', 'linear-gradient')
                .attr('x1', '0%')
                .attr('y2', '0%');
    makeElement(linearGradient, 'stop', data, 'stop-gradient')
                    .attr('offset', d => `${(d.value - domain[0]) * 100 / (domain[1] - domain[0])}%`)
                    .attr('stop-color', d => d.color);
    return linearGradient;
};

/**
 * Creates an axis cell with a linear axis for computing space and
 * creating gradient legend
 *
 * @return {AxisCell} Instance of Axis Cell for the gradient axis
 * @memberof Legend
 */
export const createAxis = (context) => {
    const data = context.data();
    const { align } = context.config();
    const AxisCell = context._cells.AxisCell;
    const newAxis = new ContinousAxis({
        id: `legend-${context._id}`,
        orientation: align === ALIGN.VERTICAL ? RIGHT : BOTTOM,
        style: context._computedStyle,
        nice: false,
        showAxisName: false,
        tickValues: data.map(d => d.value),
        fixedBaseline: false
    }, { labelManager: context._labelManager });

    newAxis.domain(getGradientDomain(data));
    newAxis.range([1, 1]);
    return new AxisCell().source(newAxis).config({
        margin: { left: 0, bottom: 0, top: 0, right: 0 }
    });
};

/**
 *
 *
 * @param {*} container
 * @param {*} data
 * @param {*} classPrefix
 *
 * @memberof GradientLegend
 */
const createLegendSkeleton = (container, classPrefix, data) => {
    const domain = getGradientDomain(data);
    const legendContainer = makeElement(container, 'div', [1], `${classPrefix}-legend-body`);
    const legendGradSvg = makeElement(legendContainer, 'svg', [1], `${classPrefix}-gradient`);
    const legendGradCont = makeElement(legendGradSvg, 'g', [1], `${classPrefix}-gradient-group`);
    const linearGradient = makeLinearGradient(legendGradSvg, data, domain);
    const legendRect = makeElement(legendGradCont, 'rect', [1], `${classPrefix}-gradient-rect`);

    return {
        legendContainer,
        legendGradCont,
        legendGradSvg,
        linearGradient,
        legendRect
    };
};

/**
 * Renders the axis for the gradient
 *
 * @param {Selection} container Point where axis is to be mounted
 * @param {number} height Height for axis
 * @param {number} width Width for axis
 * @memberof Legend
 */
export const renderAxis = (context, container, height, width) => {
    const axis = context.axis();

    axis.setAvailableSpace(width, height);
    axis.render(container.node());
};

/**
 * Renders gradient legends
 *
 * @param {Selection} container Point where the legend is to be appended
 * @memberof GradientLegend
 */
export const renderGradient = (context, container) => {
    let gradHeight = 0;
    let gradWidth = 0;
    const {
        align,
        classPrefix,
        item
    } = context.config();
    const data = context.data();
    // Create the skeleton for the legend
    const {
        legendContainer,
        legendGradSvg,
        legendGradCont,
        linearGradient,
        legendRect
    } = createLegendSkeleton(container, classPrefix, data);
    const labelDim = context.axis().source().getAxisDimensions().tickLabelDim;
    const {
        padding,
        margin,
        border,
        titleSpaces,
        maxHeight,
        maxWidth,
        height,
        width
    } = context.measurement();
    const gradientDimensions = {};

    gradHeight = Math.floor(height - (titleSpaces.height + 2 * margin + 2 * border));
    gradWidth = Math.floor(width - (margin * 2 + border * 2));

    if (align === ALIGN.HORIZONTAL) {
        gradientDimensions.height = item.icon.height;
        gradientDimensions.width = gradWidth - 2 * padding - labelDim.width / 2;
        linearGradient.attr('x2', '100%').attr('y1', '0%');
        legendGradCont.attr('transform', `translate( ${labelDim.width / 2} 0)`);
        renderAxis(context, legendContainer, gradHeight - item.icon.height - padding, gradWidth - 2 * padding - 1);
        legendContainer.classed(`${classPrefix}-overflow-x`, width > maxWidth);

        applyStyle(legendContainer, {
            height: `${height}px`,
            width: `${Math.min(width, maxWidth)}px`,
            padding: `${padding}px`
        });

        legendRect.attr('height', gradientDimensions.height);
        legendRect.attr('width', gradientDimensions.width - labelDim.width / 2);
    } else {
        gradientDimensions.height = gradHeight - 2 * padding - labelDim.height / 2;
        gradientDimensions.width = item.icon.width;
        linearGradient.attr('x2', '0%').attr('y1', '100%');
        legendGradCont.attr('transform', `translate(0 ${labelDim.height / 2})`);
        renderAxis(context, legendContainer, gradHeight - 2 * padding - 1, gradWidth - item.icon.width - padding * 2);
        legendContainer.classed(`${classPrefix}-overflow-y`, height > maxHeight);
        applyStyle(legendContainer, {
            height: `${Math.min(height, maxHeight)}px`,
            width: `${width}px`,
            padding: `${padding}px`
        });
        legendRect.attr('height', gradientDimensions.height - labelDim.height / 2);
        legendRect.attr('width', gradientDimensions.width);
    }

    // Apply Styles to the legend plot area
    applyStyle(legendGradSvg, {
        height: `${gradientDimensions.height}px`,
        width: `${gradientDimensions.width}px`
    });

        // Apply styles to the legend rect
    applyStyle(legendRect, {
        fill: 'url(#linear-gradient)'
    });
    legendGradSvg.attr('height', gradientDimensions.height);
    legendGradSvg.attr('width', gradientDimensions.width);

    context.measurement({
        gradientDimensions
    });
    context._legendGradientSvg = legendGradSvg;
    return context;
};