function templateFormatString(string, labels, d3locale) {
    var opts = this;
    var args = arguments;
    if(!labels) labels = {};

    return string.replace(lib.TEMPLATE_STRING_REGEX, function(match, rawKey, format) {
        var isOther =
            rawKey === 'xother' ||
            rawKey === 'yother';

        var isSpaceOther =
            rawKey === '_xother' ||
            rawKey === '_yother';

        var isSpaceOtherSpace =
            rawKey === '_xother_' ||
            rawKey === '_yother_';

        var isOtherSpace =
            rawKey === 'xother_' ||
            rawKey === 'yother_';

        var hasOther = isOther || isSpaceOther || isOtherSpace || isSpaceOtherSpace;

        var key = rawKey;
        if(isSpaceOther || isSpaceOtherSpace) key = key.substring(1);
        if(isOtherSpace || isSpaceOtherSpace) key = key.substring(0, key.length - 1);

        // Shape labels support * and / operators in template string
        // Parse these if the parseMultDiv param is set to true
        var parsedOp = null;
        var parsedNumber = null;
        if(opts.parseMultDiv) {
            var _match = multDivParser(key);
            key = _match.key;
            parsedOp = _match.op;
            parsedNumber = _match.number;
        }

        var value;
        if(hasOther) {
            value = labels[key];
            if(value === undefined) return '';
        } else {
            var obj, i;
            for(i = 3; i < args.length; i++) {
                obj = args[i];
                if(!obj) continue;
                if(obj.hasOwnProperty(key)) {
                    value = obj[key];
                    break;
                }

                if(!SIMPLE_PROPERTY_REGEX.test(key)) {
                    // true here means don't convert null to undefined
                    value = lib.nestedProperty(obj, key).get(true);
                }
                if(value !== undefined) break;
            }
        }

        // Apply mult/div operation (if applicable)
        if(value !== undefined) {
            if(parsedOp === '*') value *= parsedNumber;
            if(parsedOp === '/') value /= parsedNumber;
        }

        if(value === undefined && opts) {
            if(opts.count < opts.max) {
                lib.warn('Variable \'' + key + '\' in ' + opts.name + ' could not be found!');
                value = match;
            }

            if(opts.count === opts.max) {
                lib.warn('Too many ' + opts.name + ' warnings - additional warnings will be suppressed');
            }
            opts.count++;

            return match;
        }

        if(format) {
            var fmt;
            if(format[0] === ':') {
                fmt = d3locale ? d3locale.numberFormat : lib.numberFormat;
                if(value !== '') { // e.g. skip missing data on heatmap
                    value = fmt(format.replace(TEMPLATE_STRING_FORMAT_SEPARATOR, ''))(value);
                }
            }

            if(format[0] === '|') {
                fmt = d3locale ? d3locale.timeFormat : utcFormat;
                var ms = lib.dateTime2ms(value);
                value = lib.formatDate(ms, format.replace(TEMPLATE_STRING_FORMAT_SEPARATOR, ''), false, fmt);
            }
        } else {
            var keyLabel = key + 'Label';
            if(labels.hasOwnProperty(keyLabel)) value = labels[keyLabel];
        }

        if(hasOther) {
            value = '(' + value + ')';
            if(isSpaceOther || isSpaceOtherSpace) value = ' ' + value;
            if(isOtherSpace || isSpaceOtherSpace) value = value + ' ';
        }

        return value;
    });
}
Substitute values from an object into a string and optionally formats them using d3-format,
or fallback to associated labels.

Examples:
 Lib.hovertemplateString('name: %{trace}', {trace: 'asdf'}) --> 'name: asdf'
 Lib.hovertemplateString('name: %{trace[0].name}', {trace: [{name: 'asdf'}]}) --> 'name: asdf'
 Lib.hovertemplateString('price: %{y:$.2f}', {y: 1}) --> 'price: $1.00'

@param {string}  input string containing %{...:...} template strings
@param {obj}     data object containing fallback text when no formatting is specified, ex.: {yLabel: 'formattedYValue'}
@param {obj}     d3 locale
@param {obj}     data objects containing substitution values

@return {string} templated string