SpreadJS provides a SparklineRule as part of its conditional formatting, which renders sparkline charts in cells based on conditional formatting.
You can use the addSparklineRule API to create a sparkline rule. The rule supports built-in identifiers $CF_RANGE$ (refers to the cell range of the rule) and @ (represents the current cell's index or value). For the configurable options of each individual Sparkline rule, please refer to GC.Spread.Sheets.Sparklines.ISparklineOptions. For example:
Additionally, Sparkline configurable options support parameter arrays. The parameter order must align with the sequence of parameters defined in the Sparkline function's description.
var spreadNS = GC.Spread.Sheets;
window.onload = function () {
var spread = new spreadNS.Workbook(document.getElementById('ss'), { sheetCount: 4 });
spread.suspendPaint();
initSpread(spread);
initRuleControlPanel(spread);
initPanelToggle(spread);
spread.resumePaint();
};
function initSpread(spread) {
initSheet1_LollipopVariance(spread);
initSheet2_Column(spread);
initSheet3_Pie(spread);
initSheet4_Bullet(spread);
spread.setActiveSheetIndex(0);
}
/* ================================================================
Helpers
================================================================ */
function colToLetter(col) {
var s = '';
col++;
while (col > 0) {
col--;
s = String.fromCharCode(65 + (col % 26)) + s;
col = Math.floor(col / 26);
}
return s;
}
function absRange(row, col, rowCount, colCount) {
var r1 = row + 1, r2 = row + rowCount;
var c1 = colToLetter(col), c2 = colToLetter(col + colCount - 1);
return '$' + c1 + '$' + r1 + ':$' + c2 + '$' + r2;
}
// Relative-row range: column is absolute ($C), row is relative (4) — shifts per cell
function relRowRange(row, col, rowCount, colCount) {
var r1 = row + 1, r2 = row + rowCount;
var c1 = colToLetter(col), c2 = colToLetter(col + colCount - 1);
return '$' + c1 + r1 + ':$' + c2 + r2;
}
function relRowCell(row, col) {
return '$' + colToLetter(col) + (row + 1);
}
function makeStyle(opts) {
var s = new spreadNS.Style();
if (opts.font) s.font = opts.font;
if (opts.bg) s.backColor = opts.bg;
if (opts.fg) s.foreColor = opts.fg;
if (opts.hAlign !== undefined) s.hAlign = opts.hAlign;
if (opts.vAlign !== undefined) s.vAlign = opts.vAlign;
if (opts.formatter) s.formatter = opts.formatter;
if (opts.wordWrap) s.wordWrap = true;
return s;
}
/* ================================================================
Sheet 1 — Lollipop Variance (two sparkline columns: % and absolute)
================================================================ */
function initSheet1_LollipopVariance(spread) {
var sheet = spread.getSheet(0);
sheet.name('Lollipop Variance');
sheet.options.sheetTabColor = '#6fa300';
sheet.suspendPaint();
sheet.setColumnCount(8);
sheet.setRowCount(12);
var cfs = sheet.conditionalFormats;
var i, rule;
var c = 0;
var titleStyle = makeStyle({ font: 'bold 14px Calibri', fg: '#1a1a1a', vAlign: 1 });
var headerStyle = makeStyle({ font: 'bold 11px Calibri', bg: '#6fa300', fg: '#FFFFFF', hAlign: 1, vAlign: 1 });
var labelStyle = makeStyle({ bg: '#e6f2cc', font: '11px Calibri', vAlign: 1 });
var dollarStyle = makeStyle({ font: '11px Calibri', hAlign: 1, vAlign: 1, formatter: '$#,##0' });
var colorStyle = makeStyle({ font: '11px Calibri', hAlign: 1, vAlign: 1 });
var thinBorder = new spreadNS.LineBorder('#a3c760', spreadNS.LineStyle.thin);
// A:Dept B:Planned C:Actual D:Variance(%) E:Variance(Abs) F:PosColor G:NegColor
var headers = ['Department', 'Planned ($K)', 'Actual ($K)', 'Variance (%)', 'Variance (Abs)', 'Positive Color', 'Negative Color'];
var colWidths = [140, 100, 100, 180, 180, 110, 110];
// Title
var r = 1;
sheet.addSpan(r, c, 1, headers.length);
sheet.setValue(r, c, 'Budget Variance Analysis');
sheet.setStyle(r, c, titleStyle);
sheet.setRowHeight(r, 30);
// Headers
r = 2;
for (i = 0; i < headers.length; i++) {
sheet.setValue(r, c + i, headers[i]);
sheet.setStyle(r, c + i, headerStyle);
sheet.setColumnWidth(c + i, colWidths[i]);
}
sheet.setRowHeight(r, 30);
// Data: [dept, planned, actual, posColor, negColor]
var data = [
['R&D', 850, 760, '#6fa300', '#c8a000'],
['Marketing', 420, 500, '#6fa300', '#c8a000'],
['Sales', 380, 330, '#4a7a00', '#c44d2b'],
['Customer Support', 260, 305, '#4a7a00', '#c44d2b'],
['Infrastructure', 520, 475, '#6fa300', '#c8a000'],
['HR & Training', 180, 210, '#6fa300', '#c8a000'],
['Legal', 150, 128, '#4a7a00', '#c44d2b'],
['General Admin', 200, 245, '#4a7a00', '#c44d2b']
];
var dataFirstRow = r + 1;
for (i = 0; i < data.length; i++) {
var row = dataFirstRow + i;
sheet.setValue(row, c, data[i][0]);
sheet.setStyle(row, c, labelStyle);
sheet.setValue(row, c + 1, data[i][1]);
sheet.setStyle(row, c + 1, dollarStyle);
sheet.setValue(row, c + 2, data[i][2]);
sheet.setStyle(row, c + 2, dollarStyle);
// c+3, c+4 are sparkline columns (skip)
sheet.setValue(row, c + 5, data[i][3]);
sheet.setStyle(row, c + 5, colorStyle);
sheet.setValue(row, c + 6, data[i][4]);
sheet.setStyle(row, c + 6, colorStyle);
sheet.setRowHeight(row, 42);
}
// Borders
sheet.getRange(r, c, data.length + 1, headers.length).setBorder(thinBorder, { all: true });
var plannedRef = absRange(dataFirstRow, c + 1, data.length, 1);
var actualRef = absRange(dataFirstRow, c + 2, data.length, 1);
// Rule 1: Percentage variance → column D (c+3)
rule = cfs.addSparklineRule('LOLLIPOPVARISPARKLINE', {
plannedValue: plannedRef,
actualValue: actualRef,
index: '@',
reference: 0,
mini: -0.15,
maxi: 0.25,
legend: true,
colorPositive: relRowCell(dataFirstRow, c + 5),
colorNegative: relRowCell(dataFirstRow, c + 6),
lollipopHeaderColor: '#4a7a00'
}, [new spreadNS.Range(dataFirstRow, c + 3, data.length, 1)]);
if (rule) rule.showSparklineOnly(true);
// Rule 2: Absolute variance → column E (c+4)
rule = cfs.addSparklineRule('LOLLIPOPVARISPARKLINE', {
plannedValue: plannedRef,
actualValue: actualRef,
index: '@',
absolute: true,
reference: 0,
mini: -100,
maxi: 100,
legend: true,
colorPositive: relRowCell(dataFirstRow, c + 5),
colorNegative: relRowCell(dataFirstRow, c + 6),
lollipopHeaderColor: '#4a7a00'
}, [new spreadNS.Range(dataFirstRow, c + 4, data.length, 1)]);
if (rule) rule.showSparklineOnly(true);
sheet.resumePaint();
}
/* ================================================================
Sheet 2 — Column
================================================================ */
function initSheet2_Column(spread) {
var sheet = spread.getSheet(1);
sheet.name('Column');
sheet.options.sheetTabColor = '#c8a000';
sheet.suspendPaint();
sheet.setColumnCount(7);
sheet.setRowCount(10);
var cfs = sheet.conditionalFormats;
var i, j, rule;
var c = 0;
var titleStyle = makeStyle({ font: 'bold 14px Calibri', fg: '#1a1a1a', vAlign: 1 });
var headerStyle = makeStyle({ font: 'bold 11px Calibri', bg: '#c8a000', fg: '#FFFFFF', hAlign: 1, vAlign: 1 });
var labelStyle = makeStyle({ bg: '#f5f0d0', font: '11px Calibri', vAlign: 1 });
var dollarStyle = makeStyle({ font: '11px Calibri', hAlign: 1, vAlign: 1, formatter: '$#,##0' });
var thinBorder = new spreadNS.LineBorder('#d4c46a', spreadNS.LineStyle.thin);
// A:Region B-E:Q1-Q4 F:Quarterly(sparkline)
var headers = ['Region', 'Q1', 'Q2', 'Q3', 'Q4', 'Quarterly'];
var colWidths = [120, 80, 80, 80, 80, 160];
// Title
var r = 1;
sheet.addSpan(r, c, 1, headers.length);
sheet.setValue(r, c, 'Quarterly Revenue by Region');
sheet.setStyle(r, c, titleStyle);
sheet.setRowHeight(r, 30);
// Headers
r = 2;
for (i = 0; i < headers.length; i++) {
sheet.setValue(r, c + i, headers[i]);
sheet.setStyle(r, c + i, headerStyle);
sheet.setColumnWidth(c + i, colWidths[i]);
}
sheet.setRowHeight(r, 30);
// Data: [region, Q1, Q2, Q3, Q4]
var data = [
['North America', 1800, 1620, 1750, 1920],
['Europe', 1200, 1380, 1150, 1400],
['Asia Pacific', 1080, 950, 1140, 1020],
['Latin America', 480, 560, 430, 580],
['Middle East', 400, 350, 420, 370],
['Africa', 190, 260, 210, 270]
];
var dataFirstRow = r + 1;
for (i = 0; i < data.length; i++) {
var row = dataFirstRow + i;
sheet.setValue(row, c, data[i][0]);
sheet.setStyle(row, c, labelStyle);
for (j = 1; j <= 4; j++) {
sheet.setValue(row, c + j, data[i][j]);
sheet.setStyle(row, c + j, dollarStyle);
}
sheet.setRowHeight(row, 60);
}
// Borders
sheet.getRange(r, c, data.length + 1, headers.length).setBorder(thinBorder, { all: true });
// Column sparkline — single rule spanning all data rows
var setting = new GC.Spread.Sheets.Sparklines.SparklineSetting();
setting.options.seriesColor = '#4a7a00';
setting.options.showHigh = true;
setting.options.highMarkerColor = '#8b0000';
setting.options.showLow = true;
setting.options.lowMarkerColor = '#d45500';
setting.options.showFirst = true;
setting.options.firstMarkerColor = '#3d6b00';
setting.options.showLast = true;
setting.options.lastMarkerColor = '#b5cc18';
rule = cfs.addSparklineRule('COLUMNSPARKLINE', {
data: relRowRange(dataFirstRow, c + 1, 1, 4),
dataOrientation: 1,
setting: setting
}, [new spreadNS.Range(dataFirstRow, c + 5, data.length, 1)]);
if (rule) rule.showSparklineOnly(true);
sheet.resumePaint();
}
/* ================================================================
Sheet 3 — Pie
================================================================ */
function initSheet3_Pie(spread) {
var sheet = spread.getSheet(2);
sheet.name('Pie');
sheet.options.sheetTabColor = '#4a7a00';
sheet.suspendPaint();
sheet.setColumnCount(9);
sheet.setRowCount(11);
var cfs = sheet.conditionalFormats;
var i, j, rule;
var c = 0;
var titleStyle = makeStyle({ font: 'bold 14px Calibri', fg: '#1a1a1a', vAlign: 1 });
var headerStyle = makeStyle({ font: 'bold 11px Calibri', bg: '#4a7a00', fg: '#FFFFFF', hAlign: 1, vAlign: 1 });
var labelStyle = makeStyle({ bg: '#dce8c8', font: '11px Calibri', vAlign: 1 });
var pctStyle = makeStyle({ font: '11px Calibri', hAlign: 1, vAlign: 1, formatter: '0%' });
var colorStyle = makeStyle({ font: '11px Calibri', hAlign: 1, vAlign: 1 });
var thinBorder = new spreadNS.LineBorder('#8fb356', spreadNS.LineStyle.thin);
// A:Product B-D:HW%,SW%,Svc% E:Distribution(sparkline) F-H:Color1-3
var headers = ['Product', 'Hardware %', 'Software %', 'Services %', 'Distribution', 'Color 1', 'Color 2', 'Color 3'];
var colWidths = [120, 90, 90, 90, 160, 80, 80, 80];
// Title
var r = 1;
sheet.addSpan(r, c, 1, headers.length);
sheet.setValue(r, c, 'Revenue Distribution by Product');
sheet.setStyle(r, c, titleStyle);
sheet.setRowHeight(r, 30);
// Headers
r = 2;
for (i = 0; i < headers.length; i++) {
sheet.setValue(r, c + i, headers[i]);
sheet.setStyle(r, c + i, headerStyle);
sheet.setColumnWidth(c + i, colWidths[i]);
}
sheet.setRowHeight(r, 30);
// Data: [product, hw%, sw%, svc%, color1, color2, color3]
var data = [
['Laptops', 0.45, 0.35, 0.20, '#4a7a00', '#c8a000', '#6fa300'],
['Desktops', 0.30, 0.45, 0.25, '#8bc34a', '#b5cc18', '#A5A5A5'],
['Tablets', 0.55, 0.25, 0.20, '#2d5200', '#b8d86e', '#3d6b00'],
['Phones', 0.40, 0.30, 0.30, '#4a7a00', '#c8a000', '#6fa300'],
['Accessories', 0.35, 0.40, 0.25, '#d45500', '#c5e17a', '#9acd32'],
['Servers', 0.50, 0.30, 0.20, '#2d5200', '#7d9a30', '#c5d48a'],
['Networking', 0.25, 0.50, 0.25, '#4a7a00', '#c8a000', '#6fa300']
];
var dataFirstRow = r + 1;
for (i = 0; i < data.length; i++) {
var row = dataFirstRow + i;
sheet.setValue(row, c, data[i][0]);
sheet.setStyle(row, c, labelStyle);
for (j = 1; j <= 3; j++) {
sheet.setValue(row, c + j, data[i][j]);
sheet.setStyle(row, c + j, pctStyle);
}
// c+4 is sparkline column (skip)
// c+5, c+6, c+7 are color columns (plain text)
for (j = 0; j < 3; j++) {
sheet.setValue(row, c + 5 + j, data[i][4 + j]);
sheet.setStyle(row, c + 5 + j, colorStyle);
}
sheet.setRowHeight(row, 34);
}
// Borders
sheet.getRange(r, c, data.length + 1, headers.length).setBorder(thinBorder, { all: true });
// Pie sparkline — single rule spanning all data rows
// colors use relRowCell to reference color columns per row
rule = cfs.addSparklineRule('PIESPARKLINE', {
value: relRowRange(dataFirstRow, c + 1, 1, 3),
colors: [relRowCell(dataFirstRow, c + 5), relRowCell(dataFirstRow, c + 6), relRowCell(dataFirstRow, c + 7)]
}, [new spreadNS.Range(dataFirstRow, c + 4, data.length, 1)]);
if (rule) rule.showSparklineOnly(true);
sheet.resumePaint();
}
/* ================================================================
Sheet 4 — Bullet
================================================================ */
function initSheet4_Bullet(spread) {
var sheet = spread.getSheet(3);
sheet.name('Bullet');
sheet.options.sheetTabColor = '#8bc34a';
sheet.suspendPaint();
sheet.setColumnCount(9);
sheet.setRowCount(11);
var cfs = sheet.conditionalFormats;
var i, j, rule;
var c = 0;
var titleStyle = makeStyle({ font: 'bold 14px Calibri', fg: '#1a1a1a', vAlign: 1 });
var headerStyle = makeStyle({ font: 'bold 11px Calibri', bg: '#8bc34a', fg: '#FFFFFF', hAlign: 1, vAlign: 1 });
var labelStyle = makeStyle({ bg: '#eaf2d5', font: '11px Calibri', vAlign: 1 });
var dollarStyle = makeStyle({ font: '11px Calibri', hAlign: 1, vAlign: 1, formatter: '$#,##0' });
var colorStyle = makeStyle({ font: '11px Calibri', hAlign: 1, vAlign: 1 });
var thinBorder = new spreadNS.LineBorder('#b8d86e', spreadNS.LineStyle.thin);
// A:Rep B-F:Actual,Target,Max,Good,Bad G:Color
var headers = ['Sales Rep', 'Actual ($K)', 'Target ($K)', 'Max ($K)', 'Good ($K)', 'Bad ($K)', 'Color'];
var colWidths = [120, 160, 90, 80, 80, 80, 80];
// Title
var r = 1;
sheet.addSpan(r, c, 1, headers.length);
sheet.setValue(r, c, 'Sales Team Performance');
sheet.setStyle(r, c, titleStyle);
sheet.setRowHeight(r, 30);
// Headers
r = 2;
for (i = 0; i < headers.length; i++) {
sheet.setValue(r, c + i, headers[i]);
sheet.setStyle(r, c + i, headerStyle);
sheet.setColumnWidth(c + i, colWidths[i]);
}
sheet.setRowHeight(r, 30);
// Data: [name, actual, target, max, good, bad, color]
var data = [
['Alice Chen', 82, 90, 120, 90, 60, '#4a7a00'],
['Bob Martinez', 95, 85, 120, 85, 55, '#c8a000'],
['Carol Wang', 68, 80, 120, 80, 50, '#6fa300'],
['David Park', 105, 100, 130, 100, 65, '#b5cc18'],
['Eva Johnson', 73, 75, 110, 75, 45, '#8bc34a'],
['Frank Liu', 88, 95, 125, 95, 60, '#4a7a00'],
['Grace Kim', 110, 105, 135, 105, 70, '#c8a000']
];
var dataFirstRow = r + 1;
for (i = 0; i < data.length; i++) {
var row = dataFirstRow + i;
sheet.setValue(row, c, data[i][0]);
sheet.setStyle(row, c, labelStyle);
for (j = 1; j <= 5; j++) {
sheet.setValue(row, c + j, data[i][j]);
sheet.setStyle(row, c + j, dollarStyle);
}
// c+6 is color column (plain text)
sheet.setValue(row, c + 6, data[i][6]);
sheet.setStyle(row, c + 6, colorStyle);
sheet.setRowHeight(row, 34);
}
// Borders
sheet.getRange(r, c, data.length + 1, headers.length).setBorder(thinBorder, { all: true });
// Bullet sparkline → Performance column (c+6), colorScheme from Color cell (c+7)
rule = cfs.addSparklineRule('BULLETSPARKLINE', {
measure: '@',
target: relRowCell(dataFirstRow, c + 2),
maxi: relRowCell(dataFirstRow, c + 3),
good: relRowCell(dataFirstRow, c + 4),
bad: relRowCell(dataFirstRow, c + 5),
tickUnit: 20,
colorScheme: relRowCell(dataFirstRow, c + 6)
}, [new spreadNS.Range(dataFirstRow, c + 1, data.length, 1)]);
if (rule) rule.showSparklineOnly(true);
sheet.resumePaint();
}
/* ================================================================
Control Panel — Redesigned
================================================================ */
function initRuleControlPanel(spread) {
var ruleList = new RuleList(spread, document.getElementById('listItemContainer'));
var rulePanel = new RulePanel(spread);
rulePanel.bind('onRuleAdded', function () {
ruleList.update();
});
rulePanel.bind('onCancelUpdate', function () {
ruleList.clearSelectedState();
});
ruleList.bind('onRuleSelected', function (evt, args) {
if (args.rule) {
rulePanel.showUpdateMode(args.activeSheet, args.rule);
} else {
rulePanel.showAddMode();
}
});
ruleList.bind('onRuleRemoved', function () {
rulePanel.showAddMode();
});
ruleList.bind('onRuleRangeChanged', function (evt, args) {
if (args.rule && rulePanel.ruleForUpdate === args.rule) {
rulePanel.setRuleRanges(args.ranges);
}
});
spread.bind(spreadNS.Events.ActiveSheetChanged, function () {
ruleList.update();
rulePanel.showAddMode();
});
document.getElementById('format-rule-move-up').addEventListener('click', function () {
ruleList.sortFormatRules(true);
});
document.getElementById('format-rule-move-down').addEventListener('click', function () {
ruleList.sortFormatRules(false);
});
}
function initPanelToggle(spread) {
var panel = document.querySelector('.options-container');
var toggleBtn = document.getElementById('panelToggleBtn');
var collapseBtn = document.getElementById('panelCollapseBtn');
function collapsePanel() {
panel.classList.add('collapsed');
toggleBtn.classList.add('visible');
setTimeout(function () { spread.refresh(); }, 320);
}
function expandPanel() {
panel.classList.remove('collapsed');
toggleBtn.classList.remove('visible');
setTimeout(function () { spread.refresh(); }, 320);
}
collapseBtn.addEventListener('click', collapsePanel);
toggleBtn.addEventListener('click', expandPanel);
}
function cloneRanges(ranges) {
var cloned = [];
if (!ranges) return cloned;
for (var i = 0; i < ranges.length; i++) {
var range = ranges[i];
cloned.push(new spreadNS.Range(range.row, range.col, range.rowCount, range.colCount));
}
return cloned;
}
function rangesToFormula(ranges) {
var formulaParts = [];
for (var i = 0; i < ranges.length; i++) {
formulaParts.push(
spreadNS.CalcEngine.rangeToFormula(
ranges[i], 0, 0, spreadNS.CalcEngine.RangeReferenceRelative.allAbsolute
)
);
}
return formulaParts.join(',');
}
function parseRangesFormula(sheet, formula) {
if (!formula) return null;
try {
var groups = spreadNS.CalcEngine.formulaToRanges(sheet, formula, 0, 0);
var ranges = [];
if (!groups || !groups.length) return null;
for (var i = 0; i < groups.length; i++) {
var groupRanges = groups[i] && groups[i].ranges;
if (!groupRanges || !groupRanges.length) continue;
for (var j = 0; j < groupRanges.length; j++) {
ranges.push(groupRanges[j]);
}
}
return ranges.length ? cloneRanges(ranges) : null;
} catch (error) {
return null;
}
}
/* ================================================================
RulePanel — Add / Update sparkline rules
================================================================ */
function RulePanel(spread) {
this.spread = spread;
this.eventsHandler = {};
this.ruleForUpdate = null;
this.sheetForUpdate = null;
this.rangesForUpdate = null;
this.$modeLabel = document.getElementById('panelModeLabel');
this.$addBtn = document.getElementById('btnAddRule');
this.$updateBtn = document.getElementById('btnUpdateRule');
this.$cancelBtn = document.getElementById('btnCancelUpdate');
this.$sparklineType = document.getElementById('sparklineType');
this.$showOnly = document.getElementById('showSparklineOnly');
this.init();
}
RulePanel.prototype.init = function () {
var self = this;
// Build initial sparkline options UI
buildSparklineOptionsUI(self.$sparklineType.value, self.spread);
self.$sparklineType.addEventListener('change', function () {
buildSparklineOptionsUI(this.value, self.spread);
});
self.$addBtn.addEventListener('click', function () {
self.addRule();
});
self.$updateBtn.addEventListener('click', function () {
self.updateRule();
});
self.$cancelBtn.addEventListener('click', function () {
self.showAddMode();
self.trigger('onCancelUpdate');
});
};
RulePanel.prototype.addRule = function (sheet, ranges) {
var self = this;
var activeSheet = sheet || self.spread.getActiveSheet();
var targetRanges = ranges || activeSheet.getSelections();
var sparklineType = self.$sparklineType.value;
var sparklineOpts = collectSparklineOptions(sparklineType);
activeSheet.suspendPaint();
var rule = activeSheet.conditionalFormats.addSparklineRule(sparklineType, sparklineOpts, targetRanges);
if (rule) {
rule.showSparklineOnly(self.$showOnly.checked);
}
activeSheet.resumePaint();
self.trigger('onRuleAdded');
};
RulePanel.prototype.updateRule = function () {
if (!this.ruleForUpdate || !this.sheetForUpdate) return;
var ranges = cloneRanges(this.rangesForUpdate || this.ruleForUpdate.ranges());
this.sheetForUpdate.conditionalFormats.removeRule(this.ruleForUpdate);
this.addRule(this.sheetForUpdate, ranges);
this.showAddMode();
this.trigger('onCancelUpdate');
};
RulePanel.prototype.showAddMode = function () {
this.$modeLabel.textContent = 'Add Rule';
this.$addBtn.style.display = '';
this.$updateBtn.style.display = 'none';
this.$cancelBtn.style.display = 'none';
this.ruleForUpdate = null;
this.sheetForUpdate = null;
this.rangesForUpdate = null;
// Reset to defaults
this.$sparklineType.value = 'PIESPARKLINE';
buildSparklineOptionsUI('PIESPARKLINE', this.spread);
this.$showOnly.checked = false;
};
RulePanel.prototype.showUpdateMode = function (sheet, rule) {
this.ruleForUpdate = rule;
this.sheetForUpdate = sheet;
this.rangesForUpdate = cloneRanges(rule.ranges ? rule.ranges() : []);
this.$modeLabel.textContent = 'Update Rule';
this.$addBtn.style.display = 'none';
this.$updateBtn.style.display = '';
this.$cancelBtn.style.display = '';
// Sync UI with existing rule
var ruleType = rule.ruleType();
if (ruleType === spreadNS.ConditionalFormatting.RuleType.sparklineRule) {
var sparklineType = rule.sparklineType ? rule.sparklineType() : '';
var sparklineOptions = rule.sparklineOptions ? rule.sparklineOptions() : {};
this.$sparklineType.value = sparklineType;
this.$showOnly.checked = rule.showSparklineOnly ? rule.showSparklineOnly() : false;
buildSparklineOptionsUI(sparklineType, this.spread);
populateSparklineOptions(sparklineType, sparklineOptions);
}
};
RulePanel.prototype.setRuleRanges = function (ranges) {
this.rangesForUpdate = cloneRanges(ranges);
};
RulePanel.prototype.bind = function (event, callback) {
if (!this.eventsHandler[event]) this.eventsHandler[event] = [];
this.eventsHandler[event].push(callback);
};
RulePanel.prototype.trigger = function (event, args) {
var handlers = this.eventsHandler[event];
if (!handlers) return;
for (var i = 0; i < handlers.length; i++) {
handlers[i].call(this, event, args);
}
};
/* ================================================================
RuleList — Display & manage applied rules
================================================================ */
function RuleList(spread, host) {
this.spread = spread;
this.host = host;
this.conditionalRules = {};
this.eventsHandler = {};
this.update();
}
RuleList.Events = {
onRuleSelected: 'onRuleSelected',
onRuleRemoved: 'onRuleRemoved',
onRuleRangeChanged: 'onRuleRangeChanged'
};
RuleList.prototype.update = function () {
this.clear();
this.buildList();
};
RuleList.prototype.clear = function () {
this.host.innerHTML = '';
};
RuleList.prototype.clearSelectedState = function () {
var items = this.host.querySelectorAll('.ruleListItem');
for (var i = 0; i < items.length; i++) {
items[i].classList.remove('selected');
}
};
RuleList.prototype.selectRule = function (item, ruleRef) {
this.clearSelectedState();
item.classList.add('selected');
this.trigger('onRuleSelected', { activeSheet: this.spread.getActiveSheet(), rule: ruleRef });
};
RuleList.prototype.startRangeEdit = function (item, ruleRef, rangeHost) {
if (rangeHost.classList.contains('editing')) return;
var self = this;
var sheet = self.spread.getActiveSheet();
var originalFormula = rangesToFormula(ruleRef.ranges());
rangeHost.classList.add('editing');
rangeHost.innerHTML = '';
var input = document.createElement('input');
input.type = 'text';
input.className = 'ruleRangeInput';
input.value = originalFormula;
input.title = 'Press Enter to apply';
rangeHost.appendChild(input);
var completed = false;
function finish(shouldApply) {
if (completed) return;
completed = true;
rangeHost.classList.remove('editing');
var nextFormula = originalFormula;
if (shouldApply) {
var updatedRanges = parseRangesFormula(sheet, input.value.trim());
if (updatedRanges) {
sheet.suspendPaint();
ruleRef.ranges(updatedRanges);
sheet.resumePaint();
self.spread.refresh();
nextFormula = rangesToFormula(ruleRef.ranges());
self.trigger('onRuleRangeChanged', {
activeSheet: sheet,
rule: ruleRef,
ranges: cloneRanges(ruleRef.ranges())
});
} else {
window.alert('Invalid rule range. Please enter a valid range formula.');
}
}
rangeHost.textContent = nextFormula;
rangeHost.title = nextFormula;
}
input.addEventListener('click', function (e) {
e.stopPropagation();
});
input.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
finish(true);
} else if (e.key === 'Escape') {
e.preventDefault();
finish(false);
}
});
input.addEventListener('blur', function () {
finish(true);
});
setTimeout(function () {
input.focus();
input.select();
}, 0);
};
RuleList.prototype.buildList = function () {
var self = this;
var sheet = self.spread.getActiveSheet();
self.conditionalRules = sheet.conditionalFormats;
var rules = self.conditionalRules.getRules();
for (var i = rules.length - 1; i >= 0; i--) {
var rule = rules[i];
var $item = document.createElement('div');
$item.classList.add('ruleListItem');
// Description with badge
var $desc = document.createElement('div');
$desc.classList.add('ruleDescription');
var description = 'Rule';
var badgeClass = 'badge-default';
if (rule.ruleType() === spreadNS.ConditionalFormatting.RuleType.sparklineRule) {
var sparklineType = rule.sparklineType ? rule.sparklineType() : '';
var friendly = sparklineType.replace('SPARKLINE', '').replace('LOLLIPOPVARI', 'Lollipop').replace('VARI', 'Variance');
if (friendly && friendly === friendly.toUpperCase()) {
friendly = friendly.charAt(0) + friendly.slice(1).toLowerCase();
}
description = friendly || 'Sparkline';
var typeKey = friendly.toLowerCase();
if (typeKey === 'bullet') badgeClass = 'badge-bullet';
else if (typeKey === 'pie') badgeClass = 'badge-pie';
else if (typeKey === 'hbar') badgeClass = 'badge-hbar';
else if (typeKey === 'line') badgeClass = 'badge-line';
else if (typeKey === 'column') badgeClass = 'badge-column';
else if (typeKey === 'winloss') badgeClass = 'badge-winloss';
else if (typeKey === 'lollipop') badgeClass = 'badge-lollipop';
}
var badge = document.createElement('span');
badge.className = 'sparkline-badge ' + badgeClass;
badge.textContent = description;
$desc.appendChild(badge);
// Range formula
var $range = document.createElement('div');
$range.classList.add('ruleFormulas');
var formula = rangesToFormula(rule.ranges());
$range.textContent = formula;
$range.title = formula;
// Stop If True
var $stop = document.createElement('div');
$stop.classList.add('ruleStopIfTrue');
var $cb = document.createElement('input');
$cb.type = 'checkbox';
$cb.checked = rule.stopIfTrue();
if (rule.ruleType() === spreadNS.ConditionalFormatting.RuleType.sparklineRule) {
$cb.disabled = true;
}
$stop.appendChild($cb);
// Remove button
var $remove = document.createElement('div');
$remove.classList.add('ruleRemove');
var $removeBtn = document.createElement('button');
$removeBtn.className = 'removeRuleBtn';
$removeBtn.textContent = '\u00d7';
$removeBtn.title = 'Remove rule';
$remove.appendChild($removeBtn);
$item.appendChild($desc);
$item.appendChild($range);
$item.appendChild($stop);
$item.appendChild($remove);
self.host.appendChild($item);
// Event: select row
(function (item, ruleRef) {
item.addEventListener('click', function (e) {
if (e.target.classList.contains('removeRuleBtn')) return;
self.selectRule(item, ruleRef);
});
})($item, rule);
(function (item, ruleRef, rangeHost) {
rangeHost.addEventListener('click', function (e) {
e.stopPropagation();
self.selectRule(item, ruleRef);
self.startRangeEdit(item, ruleRef, rangeHost);
});
})($item, rule, $range);
// Event: remove
(function (ruleRef, removeBtn) {
removeBtn.addEventListener('click', function () {
var sheet = self.spread.getActiveSheet();
sheet.conditionalFormats.removeRule(ruleRef);
self.trigger('onRuleRemoved');
self.update();
});
})(rule, $removeBtn);
// Event: stop if true
(function (ruleRef, checkbox) {
checkbox.addEventListener('change', function () {
ruleRef.stopIfTrue(checkbox.checked);
});
})(rule, $cb);
}
};
RuleList.prototype.sortFormatRules = function (isUp) {
var self = this;
var items = self.host.querySelectorAll('.ruleListItem');
var selectedIdx = -1;
for (var i = 0; i < items.length; i++) {
if (items[i].classList.contains('selected')) { selectedIdx = i; break; }
}
if (selectedIdx === -1) return;
if ((isUp && selectedIdx === 0) || (!isUp && selectedIdx === items.length - 1)) return;
var tempFormats = $.extend(true, {}, self.conditionalRules);
self.conditionalRules.clearRule();
var total = tempFormats.count();
var indexInFormats = total - 1 - selectedIdx;
for (i = 0; i < total; i++) {
if (isUp) {
if (i === indexInFormats) self.conditionalRules.addRule(tempFormats.getRule(indexInFormats + 1));
else if (i === indexInFormats + 1) self.conditionalRules.addRule(tempFormats.getRule(indexInFormats));
else self.conditionalRules.addRule(tempFormats.getRule(i));
} else {
if (i === indexInFormats) self.conditionalRules.addRule(tempFormats.getRule(indexInFormats - 1));
else if (i === indexInFormats - 1) self.conditionalRules.addRule(tempFormats.getRule(indexInFormats));
else self.conditionalRules.addRule(tempFormats.getRule(i));
}
}
self.update();
var newItems = self.host.querySelectorAll('.ruleListItem');
var newIdx = isUp ? selectedIdx - 1 : selectedIdx + 1;
if (newItems[newIdx]) newItems[newIdx].classList.add('selected');
};
RuleList.prototype.bind = function (event, callback) {
if (!this.eventsHandler[event]) this.eventsHandler[event] = [];
this.eventsHandler[event].push(callback);
};
RuleList.prototype.trigger = function (event, args) {
var handlers = this.eventsHandler[event];
if (!handlers) return;
for (var i = 0; i < handlers.length; i++) {
handlers[i].call(this, event, args);
}
};
/* ================================================================
Sparkline Configuration & UI
================================================================ */
var sparklineConfigs = {
'PIESPARKLINE': [
{ name: 'value', label: 'Range/Percentage', type: 'range', defaultValue: '@' },
{ name: 'colors', label: 'Colors (comma separated)', type: 'text', defaultValue: '' }
],
'LINESPARKLINE': [
{ name: 'data', label: 'Data', type: 'range', defaultValue: '$CF_RANGE$' },
{ name: 'dataOrientation', label: 'Data Orientation', type: 'select', defaultValue: '0', options: ['0', '1'], optionLabels: ['Vertical', 'Horizontal'] },
{ name: 'dateAxisData', label: 'Date Axis Data', type: 'range', defaultValue: '' },
{ name: 'dateAxisOrientation', label: 'Date Axis Orientation', type: 'select', defaultValue: '0', options: ['0', '1'], optionLabels: ['Vertical', 'Horizontal'] },
{ name: 'setting.showMarkers', label: 'Show Markers', type: 'checkbox', defaultValue: false, group: 'setting' },
{ name: 'setting.showFirst', label: 'Show First', type: 'checkbox', defaultValue: false, group: 'setting' },
{ name: 'setting.showLast', label: 'Show Last', type: 'checkbox', defaultValue: false, group: 'setting' },
{ name: 'setting.showHigh', label: 'Show High', type: 'checkbox', defaultValue: false, group: 'setting' },
{ name: 'setting.showLow', label: 'Show Low', type: 'checkbox', defaultValue: false, group: 'setting' },
{ name: 'setting.showNegative', label: 'Show Negative', type: 'checkbox', defaultValue: false, group: 'setting' },
{ name: 'setting.displayXAxis', label: 'Display X Axis', type: 'checkbox', defaultValue: false, group: 'setting' },
{ name: 'setting.seriesColor', label: 'Series Color', type: 'text', defaultValue: '', group: 'setting' },
{ name: 'setting.negativeColor', label: 'Negative Color', type: 'text', defaultValue: '', group: 'setting' },
{ name: 'setting.firstMarkerColor', label: 'First Marker', type: 'text', defaultValue: '', group: 'setting' },
{ name: 'setting.lastMarkerColor', label: 'Last Marker', type: 'text', defaultValue: '', group: 'setting' },
{ name: 'setting.highMarkerColor', label: 'High Marker', type: 'text', defaultValue: '', group: 'setting' },
{ name: 'setting.lowMarkerColor', label: 'Low Marker', type: 'text', defaultValue: '', group: 'setting' },
{ name: 'setting.markersColor', label: 'Markers Color', type: 'text', defaultValue: '', group: 'setting' },
{ name: 'setting.axisColor', label: 'Axis Color', type: 'text', defaultValue: '', group: 'setting' }
],
'COLUMNSPARKLINE': [
{ name: 'data', label: 'Data', type: 'range', defaultValue: '$CF_RANGE$' },
{ name: 'dataOrientation', label: 'Data Orientation', type: 'select', defaultValue: '0', options: ['0', '1'], optionLabels: ['Vertical', 'Horizontal'] },
{ name: 'dateAxisData', label: 'Date Axis Data', type: 'range', defaultValue: '' },
{ name: 'dateAxisOrientation', label: 'Date Axis Orientation', type: 'select', defaultValue: '0', options: ['0', '1'], optionLabels: ['Vertical', 'Horizontal'] },
{ name: 'setting.showFirst', label: 'Show First', type: 'checkbox', defaultValue: false, group: 'setting' },
{ name: 'setting.showLast', label: 'Show Last', type: 'checkbox', defaultValue: false, group: 'setting' },
{ name: 'setting.showHigh', label: 'Show High', type: 'checkbox', defaultValue: false, group: 'setting' },
{ name: 'setting.showLow', label: 'Show Low', type: 'checkbox', defaultValue: false, group: 'setting' },
{ name: 'setting.showNegative', label: 'Show Negative', type: 'checkbox', defaultValue: false, group: 'setting' },
{ name: 'setting.displayXAxis', label: 'Display X Axis', type: 'checkbox', defaultValue: false, group: 'setting' },
{ name: 'setting.seriesColor', label: 'Series Color', type: 'text', defaultValue: '', group: 'setting' },
{ name: 'setting.negativeColor', label: 'Negative Color', type: 'text', defaultValue: '', group: 'setting' },
{ name: 'setting.firstMarkerColor', label: 'First Marker', type: 'text', defaultValue: '', group: 'setting' },
{ name: 'setting.lastMarkerColor', label: 'Last Marker', type: 'text', defaultValue: '', group: 'setting' },
{ name: 'setting.highMarkerColor', label: 'High Marker', type: 'text', defaultValue: '', group: 'setting' },
{ name: 'setting.lowMarkerColor', label: 'Low Marker', type: 'text', defaultValue: '', group: 'setting' },
{ name: 'setting.axisColor', label: 'Axis Color', type: 'text', defaultValue: '', group: 'setting' }
],
'WINLOSSSPARKLINE': [
{ name: 'data', label: 'Data', type: 'range', defaultValue: '$CF_RANGE$' },
{ name: 'dataOrientation', label: 'Data Orientation', type: 'select', defaultValue: '0', options: ['0', '1'], optionLabels: ['Vertical', 'Horizontal'] },
{ name: 'dateAxisData', label: 'Date Axis Data', type: 'range', defaultValue: '' },
{ name: 'dateAxisOrientation', label: 'Date Axis Orientation', type: 'select', defaultValue: '0', options: ['0', '1'], optionLabels: ['Vertical', 'Horizontal'] },
{ name: 'setting.showNegative', label: 'Show Negative', type: 'checkbox', defaultValue: false, group: 'setting' },
{ name: 'setting.displayXAxis', label: 'Display X Axis', type: 'checkbox', defaultValue: false, group: 'setting' },
{ name: 'setting.seriesColor', label: 'Series Color', type: 'text', defaultValue: '', group: 'setting' },
{ name: 'setting.negativeColor', label: 'Negative Color', type: 'text', defaultValue: '', group: 'setting' },
{ name: 'setting.axisColor', label: 'Axis Color', type: 'text', defaultValue: '', group: 'setting' }
],
'AREASPARKLINE': [
{ name: 'points', label: 'Points', type: 'range', defaultValue: '$CF_RANGE$' },
{ name: 'mini', label: 'Mini', type: 'text', defaultValue: '' },
{ name: 'maxi', label: 'Maxi', type: 'text', defaultValue: '' },
{ name: 'line1', label: 'Line1', type: 'text', defaultValue: '' },
{ name: 'line2', label: 'Line2', type: 'text', defaultValue: '' },
{ name: 'colorPositive', label: 'Color Positive', type: 'text', defaultValue: '' },
{ name: 'colorNegative', label: 'Color Negative', type: 'text', defaultValue: '' }
],
'SCATTERSPARKLINE': [
{ name: 'points1', label: 'Points1', type: 'range', defaultValue: '$CF_RANGE$' },
{ name: 'points2', label: 'Points2', type: 'range', defaultValue: '' },
{ name: 'minX', label: 'MinX', type: 'text', defaultValue: '' },
{ name: 'maxX', label: 'MaxX', type: 'text', defaultValue: '' },
{ name: 'minY', label: 'MinY', type: 'text', defaultValue: '' },
{ name: 'maxY', label: 'MaxY', type: 'text', defaultValue: '' },
{ name: 'hLine', label: 'HLine', type: 'text', defaultValue: '' },
{ name: 'vLine', label: 'VLine', type: 'text', defaultValue: '' },
{ name: 'tags', label: 'Tags', type: 'text', defaultValue: '' },
{ name: 'drawSymbol', label: 'Draw Symbol', type: 'checkbox', defaultValue: false },
{ name: 'drawLines', label: 'Draw Lines', type: 'checkbox', defaultValue: false },
{ name: 'color1', label: 'Color1', type: 'text', defaultValue: '' },
{ name: 'color2', label: 'Color2', type: 'text', defaultValue: '' },
{ name: 'dash', label: 'Dash', type: 'checkbox', defaultValue: false }
],
'BULLETSPARKLINE': [
{ name: 'measure', label: 'Measure', type: 'range', defaultValue: '@' },
{ name: 'target', label: 'Target', type: 'range', defaultValue: '' },
{ name: 'maxi', label: 'Maxi', type: 'range', defaultValue: '' },
{ name: 'good', label: 'Good', type: 'range', defaultValue: '' },
{ name: 'bad', label: 'Bad', type: 'range', defaultValue: '' },
{ name: 'forecast', label: 'Forecast', type: 'range', defaultValue: '' },
{ name: 'tickUnit', label: 'Tick Unit', type: 'range', defaultValue: '' },
{ name: 'colorScheme', label: 'Color Scheme', type: 'text', defaultValue: '' },
{ name: 'vertical', label: 'Vertical', type: 'checkbox', defaultValue: false },
{ name: 'measureColor', label: 'Measure Color', type: 'text', defaultValue: '' },
{ name: 'targetColor', label: 'Target Color', type: 'text', defaultValue: '' },
{ name: 'maxiColor', label: 'Maxi Color', type: 'text', defaultValue: '' },
{ name: 'goodColor', label: 'Good Color', type: 'text', defaultValue: '' },
{ name: 'badColor', label: 'Bad Color', type: 'text', defaultValue: '' },
{ name: 'forecastColor', label: 'Forecast Color', type: 'text', defaultValue: '' },
{ name: 'allowMeasureOverMaxi', label: 'Allow Over Maxi', type: 'checkbox', defaultValue: false },
{ name: 'barSize', label: 'Bar Size', type: 'text', defaultValue: '' }
],
'SPREADSPARKLINE': [
{ name: 'points', label: 'Points', type: 'range', defaultValue: '$CF_RANGE$' },
{ name: 'showAverage', label: 'Show Average', type: 'checkbox', defaultValue: false },
{ name: 'scaleStart', label: 'Scale Start', type: 'text', defaultValue: '' },
{ name: 'scaleEnd', label: 'Scale End', type: 'text', defaultValue: '' },
{ name: 'style', label: 'Style (1-6)', type: 'text', defaultValue: '1' },
{ name: 'colorScheme', label: 'Color Scheme', type: 'text', defaultValue: '' },
{ name: 'vertical', label: 'Vertical', type: 'checkbox', defaultValue: false }
],
'STACKEDSPARKLINE': [
{ name: 'points', label: 'Points', type: 'range', defaultValue: '$CF_RANGE$' },
{ name: 'colorRange', label: 'Color Range', type: 'range', defaultValue: '' },
{ name: 'labelRange', label: 'Label Range', type: 'range', defaultValue: '' },
{ name: 'maximum', label: 'Maximum', type: 'text', defaultValue: '' },
{ name: 'targetRed', label: 'Target Red', type: 'text', defaultValue: '' },
{ name: 'targetGreen', label: 'Target Green', type: 'text', defaultValue: '' },
{ name: 'targetBlue', label: 'Target Blue', type: 'text', defaultValue: '' },
{ name: 'targetYellow', label: 'Target Yellow', type: 'text', defaultValue: '' },
{ name: 'color', label: 'Color', type: 'text', defaultValue: '' },
{ name: 'highlightPosition', label: 'Highlight Position', type: 'text', defaultValue: '' },
{ name: 'vertical', label: 'Vertical', type: 'checkbox', defaultValue: false },
{ name: 'textOrientation', label: 'Text Orientation', type: 'text', defaultValue: '0' },
{ name: 'textSize', label: 'Text Size', type: 'text', defaultValue: '10' }
],
'HBARSPARKLINE': [
{ name: 'value', label: 'Value', type: 'range', defaultValue: '@' },
{ name: 'colorScheme', label: 'Color Scheme', type: 'text', defaultValue: 'gray' },
{ name: 'axisVisible', label: 'Axis Visible', type: 'checkbox', defaultValue: true },
{ name: 'barHeight', label: 'Bar Height', type: 'text', defaultValue: '0.8' }
],
'VBARSPARKLINE': [
{ name: 'value', label: 'Value', type: 'range', defaultValue: '@' },
{ name: 'colorScheme', label: 'Color Scheme', type: 'text', defaultValue: 'gray' },
{ name: 'axisVisible', label: 'Axis Visible', type: 'checkbox', defaultValue: true },
{ name: 'barWidth', label: 'Bar Width', type: 'text', defaultValue: '0.8' }
],
'VARISPARKLINE': [
{ name: 'variance', label: 'Variance', type: 'range', defaultValue: '@' },
{ name: 'reference', label: 'Reference', type: 'range', defaultValue: '' },
{ name: 'mini', label: 'Mini', type: 'range', defaultValue: '' },
{ name: 'maxi', label: 'Maxi', type: 'range', defaultValue: '' },
{ name: 'mark', label: 'Mark', type: 'range', defaultValue: '' },
{ name: 'tickUnit', label: 'Tick Unit', type: 'range', defaultValue: '' },
{ name: 'legend', label: 'Legend', type: 'checkbox', defaultValue: false },
{ name: 'colorPositive', label: 'Color Positive', type: 'text', defaultValue: '' },
{ name: 'colorNegative', label: 'Color Negative', type: 'text', defaultValue: '' },
{ name: 'vertical', label: 'Vertical', type: 'checkbox', defaultValue: false }
],
'LOLLIPOPVARISPARKLINE': [
{ name: 'plannedValue', label: 'Planned Value', type: 'range', defaultValue: '' },
{ name: 'actualValue', label: 'Actual Value', type: 'range', defaultValue: '$CF_RANGE$' },
{ name: 'index', label: 'Index', type: 'range', defaultValue: '@' },
{ name: 'absolute', label: 'Absolute', type: 'checkbox', defaultValue: false },
{ name: 'reference', label: 'Reference', type: 'range', defaultValue: '' },
{ name: 'mini', label: 'Mini', type: 'range', defaultValue: '' },
{ name: 'maxi', label: 'Maxi', type: 'range', defaultValue: '' },
{ name: 'tickUnit', label: 'Tick Unit', type: 'range', defaultValue: '' },
{ name: 'legend', label: 'Legend', type: 'checkbox', defaultValue: false },
{ name: 'colorPositive', label: 'Color Positive', type: 'text', defaultValue: '' },
{ name: 'colorNegative', label: 'Color Negative', type: 'text', defaultValue: '' },
{ name: 'lollipopHeaderColor', label: 'Header Color', type: 'text', defaultValue: '' },
{ name: 'vertical', label: 'Vertical', type: 'checkbox', defaultValue: false }
],
'BOXPLOTSPARKLINE': [
{ name: 'points', label: 'Points', type: 'range', defaultValue: '$CF_RANGE$' },
{ name: 'boxPlotClass', label: 'BoxPlot Class', type: 'select', defaultValue: '5ns', options: ['5ns', '7ns', 'tukey', 'bowley', 'sigma3'] },
{ name: 'showAverage', label: 'Show Average', type: 'checkbox', defaultValue: false },
{ name: 'scaleStart', label: 'Scale Start', type: 'text', defaultValue: '' },
{ name: 'scaleEnd', label: 'Scale End', type: 'text', defaultValue: '' },
{ name: 'acceptableStart', label: 'Acceptable Start', type: 'text', defaultValue: '' },
{ name: 'acceptableEnd', label: 'Acceptable End', type: 'text', defaultValue: '' },
{ name: 'colorScheme', label: 'Color Scheme', type: 'text', defaultValue: '' },
{ name: 'style', label: 'Style (0 or 1)', type: 'text', defaultValue: '0' },
{ name: 'vertical', label: 'Vertical', type: 'checkbox', defaultValue: false }
],
'CASCADESPARKLINE': [
{ name: 'pointsRange', label: 'Points Range', type: 'range', defaultValue: '$CF_RANGE$' },
{ name: 'pointIndex', label: 'Point Index', type: 'range', defaultValue: '@' },
{ name: 'labelsRange', label: 'Labels Range', type: 'range', defaultValue: '' },
{ name: 'minimum', label: 'Minimum', type: 'text', defaultValue: '' },
{ name: 'maximum', label: 'Maximum', type: 'text', defaultValue: '' },
{ name: 'colorPositive', label: 'Color Positive', type: 'text', defaultValue: '' },
{ name: 'colorNegative', label: 'Color Negative', type: 'text', defaultValue: '' },
{ name: 'vertical', label: 'Vertical', type: 'checkbox', defaultValue: false },
{ name: 'itemTypeRange', label: 'Item Type Range', type: 'range', defaultValue: '' },
{ name: 'colorTotal', label: 'Color Total', type: 'text', defaultValue: '' }
],
'PARETOSPARKLINE': [
{ name: 'points', label: 'Points', type: 'range', defaultValue: '$CF_RANGE$' },
{ name: 'pointIndex', label: 'Point Index', type: 'range', defaultValue: '@' },
{ name: 'colorRange', label: 'Color Range', type: 'range', defaultValue: '' },
{ name: 'target', label: 'Target', type: 'range', defaultValue: '' },
{ name: 'target2', label: 'Target2', type: 'range', defaultValue: '' },
{ name: 'highlightPosition', label: 'Highlight Position', type: 'range', defaultValue: '' },
{ name: 'label', label: 'Label (0:None,1:Number,2:Percent)', type: 'text', defaultValue: '0' },
{ name: 'vertical', label: 'Vertical', type: 'checkbox', defaultValue: false },
{ name: 'targetColor', label: 'Target Color', type: 'text', defaultValue: '' },
{ name: 'target2Color', label: 'Target2 Color', type: 'text', defaultValue: '' },
{ name: 'labelColor', label: 'Label Color', type: 'text', defaultValue: '' },
{ name: 'barSize', label: 'Bar Size', type: 'text', defaultValue: '' }
],
'MONTHSPARKLINE': [
{ name: 'year', label: 'Year', type: 'text', defaultValue: new Date().getFullYear().toString() },
{ name: 'month', label: 'Month', type: 'text', defaultValue: '1' },
{ name: 'dataRange', label: 'Data Range', type: 'range', defaultValue: '$CF_RANGE$' },
{ name: 'emptyColor', label: 'Empty Color', type: 'text', defaultValue: '' },
{ name: 'startColor', label: 'Start Color', type: 'text', defaultValue: '' },
{ name: 'middleColor', label: 'Middle Color', type: 'text', defaultValue: '' },
{ name: 'endColor', label: 'End Color', type: 'text', defaultValue: '' },
{ name: 'colorRange', label: 'Color Range', type: 'range', defaultValue: '' }
],
'YEARSPARKLINE': [
{ name: 'year', label: 'Year', type: 'text', defaultValue: new Date().getFullYear().toString() },
{ name: 'dataRange', label: 'Data Range', type: 'range', defaultValue: '$CF_RANGE$' },
{ name: 'emptyColor', label: 'Empty Color', type: 'text', defaultValue: '' },
{ name: 'startColor', label: 'Start Color', type: 'text', defaultValue: '' },
{ name: 'middleColor', label: 'Middle Color', type: 'text', defaultValue: '' },
{ name: 'endColor', label: 'End Color', type: 'text', defaultValue: '' },
{ name: 'colorRange', label: 'Color Range', type: 'range', defaultValue: '' }
],
'HISTOGRAMSPARKLINE': [
{ name: 'dataRange', label: 'Data Range', type: 'range', defaultValue: '$CF_RANGE$' },
{ name: 'continuous', label: 'Continuous', type: 'checkbox', defaultValue: false },
{ name: 'paintLabel', label: 'Paint Label', type: 'checkbox', defaultValue: false },
{ name: 'scale', label: 'Scale', type: 'text', defaultValue: '' },
{ name: 'barWidth', label: 'Bar Width', type: 'text', defaultValue: '' },
{ name: 'barColor', label: 'Bar Color', type: 'text', defaultValue: '' },
{ name: 'labelFontStyle', label: 'Label Font Style', type: 'text', defaultValue: '' },
{ name: 'labelColor', label: 'Label Color', type: 'text', defaultValue: '' },
{ name: 'edgeColor', label: 'Edge Color', type: 'text', defaultValue: '' }
],
'GAUGEKPISPARKLINE': [
{ name: 'targetValue', label: 'Target Value', type: 'range', defaultValue: '' },
{ name: 'currentValue', label: 'Current Value', type: 'range', defaultValue: '@' },
{ name: 'minValue', label: 'Min Value', type: 'range', defaultValue: '0' },
{ name: 'maxValue', label: 'Max Value', type: 'range', defaultValue: '100' },
{ name: 'showLabel', label: 'Show Label', type: 'checkbox', defaultValue: false },
{ name: 'targetValueLabel', label: 'Target Label', type: 'text', defaultValue: '' },
{ name: 'currentValueLabel', label: 'Current Label', type: 'text', defaultValue: '' },
{ name: 'minValueLabel', label: 'Min Label', type: 'text', defaultValue: '' },
{ name: 'maxValueLabel', label: 'Max Label', type: 'text', defaultValue: '' },
{ name: 'minAngle', label: 'Min Angle', type: 'text', defaultValue: '-90' },
{ name: 'maxAngle', label: 'Max Angle', type: 'text', defaultValue: '90' },
{ name: 'radiusRatio', label: 'Radius Ratio', type: 'text', defaultValue: '' },
{ name: 'gaugeType', label: 'Gauge Type (0,1,2)', type: 'text', defaultValue: '' }
],
'IMAGE': [
{ name: 'source', label: 'Source (URL)', type: 'range', defaultValue: '@' },
{ name: 'alt_text', label: 'Alt Text', type: 'text', defaultValue: '' },
{ name: 'sizing', label: 'Sizing (0-3)', type: 'text', defaultValue: '0' },
{ name: 'height', label: 'Height', type: 'text', defaultValue: '' },
{ name: 'width', label: 'Width', type: 'text', defaultValue: '' },
{ name: 'clipX', label: 'Clip X', type: 'text', defaultValue: '' },
{ name: 'clipY', label: 'Clip Y', type: 'text', defaultValue: '' },
{ name: 'clipHeight', label: 'Clip Height', type: 'text', defaultValue: '' },
{ name: 'clipWidth', label: 'Clip Width', type: 'text', defaultValue: '' },
{ name: 'vAlign', label: 'VAlign (0-2)', type: 'text', defaultValue: '1' },
{ name: 'hAlign', label: 'HAlign (0-2)', type: 'text', defaultValue: '1' }
],
'RANGEBLOCKSPARKLINE': [
{ name: 'template_range', label: 'Template Range', type: 'range', defaultValue: '' },
{ name: 'data_expression', label: 'Data Expression (JSON)', type: 'text', defaultValue: '' }
]
};
var sparklineFormulaTextBoxes = {};
function buildSparklineOptionsUI(sparklineType, spread) {
var container = document.getElementById('sparklineOptions');
for (var key in sparklineFormulaTextBoxes) {
if (sparklineFormulaTextBoxes.hasOwnProperty(key) && sparklineFormulaTextBoxes[key].destroy) {
sparklineFormulaTextBoxes[key].destroy();
}
}
sparklineFormulaTextBoxes = {};
container.innerHTML = '';
var config = sparklineConfigs[sparklineType];
if (!config) return;
var table = document.createElement('table');
table.style.width = '100%';
var lastGroup = null;
for (var i = 0; i < config.length; i++) {
var param = config[i];
if (param.group && param.group !== lastGroup) {
var trH = document.createElement('tr');
trH.className = 'group-header';
var tdH = document.createElement('td');
tdH.colSpan = 2;
tdH.innerText = 'Sparkline Setting';
trH.appendChild(tdH);
table.appendChild(trH);
lastGroup = param.group;
}
var tr = document.createElement('tr');
var tdLabel = document.createElement('td');
tdLabel.className = 'opt-label';
tdLabel.innerText = param.label;
var tdInput = document.createElement('td');
if (param.type === 'checkbox') {
var cb = document.createElement('input');
cb.type = 'checkbox';
cb.id = 'sp_' + param.name;
cb.checked = !!param.defaultValue;
tdInput.appendChild(cb);
} else if (param.type === 'select') {
var sel = document.createElement('select');
sel.id = 'sp_' + param.name;
sel.className = 'ruleOptionsInput';
for (var j = 0; j < param.options.length; j++) {
var opt = document.createElement('option');
opt.value = param.options[j];
opt.text = param.optionLabels ? param.optionLabels[j] : param.options[j];
if (param.options[j] === param.defaultValue) opt.selected = true;
sel.appendChild(opt);
}
tdInput.appendChild(sel);
} else if (param.type === 'range') {
var div = document.createElement('div');
div.id = 'sp_' + param.name;
div.className = 'ruleOptionsFormulaTextBox';
tdInput.appendChild(div);
} else {
var input = document.createElement('input');
input.type = 'text';
input.id = 'sp_' + param.name;
input.className = 'ruleOptionsInput';
input.value = param.defaultValue || '';
tdInput.appendChild(input);
}
tr.appendChild(tdLabel);
tr.appendChild(tdInput);
table.appendChild(tr);
}
container.appendChild(table);
for (var k = 0; k < config.length; k++) {
var p = config[k];
if (p.type === 'range') {
var el = document.getElementById('sp_' + p.name);
if (el && spread) {
var ftb = new spreadNS.FormulaTextBox.FormulaTextBox(el, {
absoluteReference: true,
needSheetName: false,
rangeSelectMode: true
});
ftb.workbook(spread);
if (p.defaultValue) ftb.text(p.defaultValue);
sparklineFormulaTextBoxes[p.name] = ftb;
}
}
}
}
var sparklineStringFields = [
'data', 'points', 'points1', 'points2', 'pointsRange', 'dataRange',
'plannedValue', 'actualValue', 'colorRange', 'labelRange', 'labelsRange',
'itemTypeRange', 'template_range', 'data_expression', 'tags', 'colors',
'colorScheme', 'boxPlotClass', 'source', 'alt_text', 'labelFontStyle',
'dateAxisData', 'value',
'targetValueLabel', 'currentValueLabel', 'minValueLabel', 'maxValueLabel'
];
function collectSparklineOptions(sparklineType) {
var config = sparklineConfigs[sparklineType];
if (!config) return {};
var options = {};
var settingOptions = {};
var hasSetting = false;
for (var i = 0; i < config.length; i++) {
var param = config[i];
var val;
if (param.type === 'checkbox') {
var el = document.getElementById('sp_' + param.name);
if (!el) continue;
val = el.checked;
} else if (param.type === 'range') {
var ftb = sparklineFormulaTextBoxes[param.name];
if (!ftb) continue;
val = ftb.text();
} else {
var el2 = document.getElementById('sp_' + param.name);
if (!el2) continue;
val = el2.value;
}
if (param.group === 'setting') {
var settingKey = param.name.replace('setting.', '');
if (param.type === 'checkbox') {
if (val) { settingOptions[settingKey] = val; hasSetting = true; }
} else if (val !== '' && val !== null && val !== undefined) {
settingOptions[settingKey] = val; hasSetting = true;
}
continue;
}
if (param.type !== 'checkbox' && (val === '' || val === null || val === undefined)) continue;
if (param.type === 'checkbox' && !val) continue;
if (param.name === 'colors' && val) {
var colorArr = val.split(',').map(function (c) { return c.trim(); }).filter(function (c) { return c; });
if (colorArr.length > 0) options[param.name] = colorArr;
continue;
}
if (param.type === 'text' && sparklineStringFields.indexOf(param.name) === -1 &&
val !== '@' && val !== '$CF_RANGE$') {
if (typeof val === 'string' && val.charAt(0) !== '#' && !isColorName(val)) {
var numVal = parseFloat(val);
if (!isNaN(numVal) && isFinite(numVal)) val = numVal;
}
}
if (param.type === 'select' && val !== '') {
var selNum = parseFloat(val);
if (!isNaN(selNum) && isFinite(selNum)) val = selNum;
}
options[param.name] = val;
}
if (hasSetting) {
var setting = new GC.Spread.Sheets.Sparklines.SparklineSetting();
for (var key in settingOptions) {
if (settingOptions.hasOwnProperty(key)) setting.options[key] = settingOptions[key];
}
options['setting'] = setting;
}
return options;
}
function isColorName(val) {
var colors = ['red', 'green', 'blue', 'yellow', 'orange', 'purple', 'pink', 'black', 'white', 'gray', 'grey',
'cyan', 'magenta', 'brown', 'lime', 'navy', 'teal', 'maroon', 'olive', 'aqua', 'silver', 'fuchsia'];
return colors.indexOf(val.toLowerCase()) >= 0;
}
function populateSparklineOptions(sparklineType, sparklineOptions) {
var config = sparklineConfigs[sparklineType];
if (!config || !sparklineOptions) return;
for (var i = 0; i < config.length; i++) {
var param = config[i];
var val;
if (param.group === 'setting') {
var settingObj = sparklineOptions['setting'];
if (settingObj) {
var settingKey = param.name.replace('setting.', '');
var opts = settingObj.options || settingObj;
val = opts[settingKey];
} else {
val = undefined;
}
} else if (param.name === 'colors' && Array.isArray(sparklineOptions[param.name])) {
val = sparklineOptions[param.name].join(', ');
} else {
val = sparklineOptions[param.name];
}
if (val === undefined || val === null) continue;
if (param.type === 'checkbox') {
var el = document.getElementById('sp_' + param.name);
if (el) el.checked = !!val;
} else if (param.type === 'range') {
var ftb = sparklineFormulaTextBoxes[param.name];
if (ftb) ftb.text(val + '');
} else {
var el2 = document.getElementById('sp_' + param.name);
if (el2) el2.value = val;
}
}
}
<!doctype html>
<html style="height:100%;font-size:14px;">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="$DEMOROOT$/en/purejs/node_modules/@mescius/spread-sheets/styles/gc.spread.sheets.excel2013white.css">
<script src="$DEMOROOT$/en/purejs/node_modules/@mescius/spread-sheets/dist/gc.spread.sheets.all.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/spread/source/js/license.js" type="text/javascript"></script>
<script src="$DEMOROOT$/spread/source/js/jquery-1.8.2.js" type="text/javascript"></script>
<script src="app.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<div class="sample-tutorial">
<div id="ss" class="sample-spreadsheets"></div>
<div id="panelToggleBtn" class="panel-toggle-btn" title="Toggle Panel">
<span class="panel-toggle-icon"><span></span></span>
</div>
<div class="options-container">
<div class="panel-header">
<span class="panel-title">Sparkline Rules</span>
<span id="panelCollapseBtn" class="panel-collapse-btn" title="Collapse">×</span>
</div>
<div class="panel-section">
<div class="panel-section-title">
<span>Rule List</span>
<div class="rule-order-btns">
<button id="format-rule-move-up" class="btn-icon" title="Move Up">
<span class="icon-up"></span>
</button>
<button id="format-rule-move-down" class="btn-icon" title="Move Down">
<span class="icon-down"></span>
</button>
</div>
</div>
<div id="listContainer" class="rule-list-container">
<div id="listControlBar" class="rule-list-header">
<div class="rl-col rl-col-type">Type</div>
<div class="rl-col rl-col-range">Range</div>
<div class="rl-col rl-col-stop">Stop If True</div>
<div class="rl-col rl-col-action"></div>
</div>
<div id="listItemContainer" class="rule-list-body">
</div>
</div>
</div>
<div class="panel-section panel-section-editor">
<div class="panel-section-title">
<span>Rule Option Setting</span>
<div class="panel-editor-mode">
<span id="panelModeLabel">Add Rule</span>
<button id="btnCancelUpdate" class="btn-link" style="display:none;">Cancel</button>
</div>
</div>
<div class="panel-section-hint">Select a rule above to edit its options. Click a rule range to edit it directly.</div>
<div class="panel-card">
<div class="form-group form-group-inline">
<label class="form-label">Sparkline Type</label>
<select id="sparklineType" class="form-select">
<option value="PIESPARKLINE">Pie</option>
<option value="LINESPARKLINE">Line</option>
<option value="COLUMNSPARKLINE">Column</option>
<option value="WINLOSSSPARKLINE">WinLoss</option>
<option value="AREASPARKLINE">Area</option>
<option value="SCATTERSPARKLINE">Scatter</option>
<option value="BULLETSPARKLINE">Bullet</option>
<option value="SPREADSPARKLINE">Spread</option>
<option value="STACKEDSPARKLINE">Stacked</option>
<option value="HBARSPARKLINE">HBar</option>
<option value="VBARSPARKLINE">VBar</option>
<option value="VARISPARKLINE">Variance</option>
<option value="LOLLIPOPVARISPARKLINE">Lollipop Variance</option>
<option value="BOXPLOTSPARKLINE">BoxPlot</option>
<option value="CASCADESPARKLINE">Cascade</option>
<option value="PARETOSPARKLINE">Pareto</option>
<option value="MONTHSPARKLINE">Month</option>
<option value="YEARSPARKLINE">Year</option>
<option value="HISTOGRAMSPARKLINE">Histogram</option>
<option value="GAUGEKPISPARKLINE">GaugeKPI</option>
<option value="IMAGE">Image</option>
<option value="RANGEBLOCKSPARKLINE">RangeBlock</option>
</select>
</div>
<div id="sparklineOptions" class="sparkline-options-area"></div>
<label class="form-checkbox">
<input type="checkbox" id="showSparklineOnly"/>
<span>Show Sparkline Only</span>
</label>
<div class="panel-actions">
<button id="btnAddRule" class="btn btn-primary">Add Rule</button>
<button id="btnUpdateRule" class="btn btn-primary" style="display:none;">Update Rule</button>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
body {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.sample-tutorial {
position: relative;
height: 100%;
overflow: hidden;
}
.sample-spreadsheets {
width: 100%;
height: 100%;
overflow: hidden;
}
.options-container {
position: absolute;
top: 0;
right: 0;
width: 480px;
overflow-y: auto;
overflow-x: hidden;
height: 100%;
box-sizing: border-box;
background: #fff;
z-index: 1000;
box-shadow: -2px 0 12px rgba(0,0,0,0.10);
transition: transform 0.3s ease;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
color: #333;
}
.options-container.collapsed {
transform: translateX(100%);
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid #e8e8e8;
background: #fff;
position: sticky;
top: 0;
z-index: 2;
}
.panel-title {
font-weight: 600;
font-size: 15px;
color: #1a1a1a;
}
.panel-collapse-btn {
cursor: pointer;
font-size: 20px;
color: #999;
width: 28px;
height: 28px;
line-height: 28px;
text-align: center;
border-radius: 6px;
user-select: none;
transition: all 0.15s;
}
.panel-collapse-btn:hover {
background: #f0f0f0;
color: #333;
}
.panel-toggle-btn {
position: absolute;
top: 8px;
right: 8px;
z-index: 999;
width: 36px;
height: 36px;
background: #4a7a00;
border-radius: 8px;
cursor: pointer;
display: none;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(74,122,0,0.3);
transition: background 0.2s;
}
.panel-toggle-btn:hover { background: #3d6b00; }
.panel-toggle-btn.visible { display: flex; }
.panel-toggle-icon {
width: 18px;
height: 14px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.panel-toggle-icon::before,
.panel-toggle-icon::after,
.panel-toggle-icon span {
content: '';
display: block;
height: 2px;
background: #fff;
border-radius: 1px;
}
.panel-section {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
}
.panel-section:last-child {
border-bottom: none;
}
.panel-section-title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
font-weight: 600;
font-size: 13px;
color: #555;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.panel-editor-mode {
display: flex;
align-items: center;
gap: 10px;
}
.panel-section-hint {
margin-bottom: 10px;
font-size: 12px;
line-height: 1.4;
color: #7a7a7a;
}
.panel-card {
background: #fafafa;
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 14px;
}
.form-group {
margin-bottom: 12px;
}
.form-group-inline {
display: flex;
align-items: center;
}
.form-group-inline .form-label {
display: inline-block;
width: 140px;
min-width: 140px;
margin-bottom: 0;
padding: 0 4px;
box-sizing: border-box;
}
.form-group-inline .form-select {
width: 180px;
margin-left: 10px;
}
.form-label {
display: block;
font-size: 12px;
font-weight: 500;
color: #666;
margin-bottom: 4px;
}
.form-select {
width: 100%;
height: 32px;
padding: 0 8px;
border: 1px solid #d0d0d0;
border-radius: 6px;
background: #fff;
font-size: 13px;
color: #333;
outline: none;
box-sizing: border-box;
transition: border-color 0.15s;
cursor: pointer;
}
.form-select:focus {
border-color: #4a7a00;
box-shadow: 0 0 0 2px rgba(74,122,0,0.15);
}
.form-input {
width: 100%;
height: 30px;
padding: 0 8px;
border: 1px solid #d0d0d0;
border-radius: 6px;
background: #fff;
font-size: 13px;
color: #333;
outline: none;
box-sizing: border-box;
transition: border-color 0.15s;
}
.form-input:focus {
border-color: #4a7a00;
box-shadow: 0 0 0 2px rgba(74,122,0,0.15);
}
.form-checkbox {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
margin: 8px 0;
font-size: 13px;
color: #444;
}
.form-checkbox input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: #4a7a00;
cursor: pointer;
}
.sparkline-options-area {
max-height: 220px;
overflow-y: auto;
margin: 8px 0;
}
.sparkline-options-area table {
width: 100%;
border-collapse: collapse;
}
.sparkline-options-area td {
padding: 3px 4px;
vertical-align: middle;
font-size: 12px;
}
.sparkline-options-area .opt-label {
width: 140px;
color: #666;
white-space: nowrap;
}
.sparkline-options-area .group-header td {
font-weight: 600;
color: #4a7a00;
padding-top: 8px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.ruleOptionsInput {
border: 1px solid #d0d0d0;
height: 28px;
line-height: 28px;
padding: 0 6px;
border-radius: 4px;
font-size: 12px;
box-sizing: border-box;
outline: none;
width: 180px;
transition: border-color 0.15s;
}
.ruleOptionsInput:focus {
border-color: #4a7a00;
}
.ruleOptionsFormulaTextBox {
display: inline-block;
border: 1px solid #d0d0d0;
width: 180px;
height: 28px;
vertical-align: middle;
border-radius: 4px;
box-sizing: border-box;
overflow: hidden;
}
.ruleOptionsFormulaTextBox > table {
width: 100% !important;
table-layout: fixed;
}
.ruleOptionsFormulaTextBox div[gcuielement="gcAttachedFormulaTextBox"] {
width: 100% !important;
}
.ruleOptionsFormulaTextBox td:first-child > div {
width: calc(100% - 2px) !important;
}
.panel-actions {
margin-top: 12px;
display: flex;
gap: 8px;
}
.btn {
height: 34px;
padding: 0 20px;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
}
.btn-primary {
background: #4a7a00;
color: #fff;
}
.btn-primary:hover {
background: #3d6b00;
}
.btn-link {
background: none;
border: none;
color: #4a7a00;
cursor: pointer;
font-size: 12px;
padding: 0;
font-weight: 500;
}
.btn-link:hover {
text-decoration: underline;
}
.btn-icon {
width: 28px;
height: 28px;
padding: 0;
border: 1px solid #d0d0d0;
border-radius: 6px;
background: #fff;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.btn-icon:hover {
background: #f0f0f0;
border-color: #bbb;
}
.rule-order-btns {
display: flex;
gap: 4px;
}
.icon-up, .icon-down {
display: block;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
}
.icon-up {
border-bottom: 6px solid #666;
}
.icon-down {
border-top: 6px solid #666;
}
.rule-list-container {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
background: #fff;
}
.rule-list-header {
display: flex;
background: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
font-size: 11px;
font-weight: 600;
color: #777;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.rule-list-body {
max-height: 240px;
overflow-y: auto;
}
.rl-col {
padding: 8px 10px;
box-sizing: border-box;
}
.rl-col-type { width: 40%; }
.rl-col-range { width: 28%; }
.rl-col-stop { width: 18%; text-align: center; }
.rl-col-action { width: 14%; text-align: center; }
.ruleListItem {
display: flex;
align-items: center;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background 0.1s;
}
.ruleListItem:last-child { border-bottom: none; }
.ruleListItem:hover { background: #f8fbf0; }
.ruleListItem.selected { background: #eef5e0; }
.ruleDescription {
width: 40%;
padding: 6px 10px;
font-size: 12px;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
box-sizing: border-box;
}
.ruleFormulas {
width: 28%;
padding: 6px 6px;
font-size: 11px;
color: #4a7a00;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
box-sizing: border-box;
cursor: text;
border-radius: 4px;
transition: background 0.15s, color 0.15s;
}
.ruleFormulas:hover {
background: #f1f7e6;
}
.ruleFormulas.editing {
padding: 4px 6px;
background: #eef5e0;
}
.ruleStopIfTrue {
width: 18%;
text-align: center;
padding: 6px 4px;
box-sizing: border-box;
}
.ruleStopIfTrue input[type="checkbox"] {
accent-color: #4a7a00;
}
.ruleRemove {
width: 14%;
text-align: center;
padding: 6px 4px;
box-sizing: border-box;
}
.removeRuleBtn {
width: 24px;
height: 24px;
border: none;
background: none;
color: #ccc;
font-size: 16px;
cursor: pointer;
border-radius: 4px;
line-height: 24px;
transition: all 0.15s;
}
.removeRuleBtn:hover {
background: #fee;
color: #e55;
}
.ruleRangeInput {
width: 100%;
height: 28px;
padding: 0 8px;
border: 1px solid #8fb44a;
border-radius: 4px;
background: #fff;
color: #333;
font-size: 11px;
box-sizing: border-box;
outline: none;
}
.ruleRangeInput:focus {
box-shadow: 0 0 0 2px rgba(74,122,0,0.15);
}
.sparkline-badge {
display: inline-block;
padding: 1px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: 600;
margin-right: 6px;
vertical-align: middle;
color: #fff;
}
.badge-bullet { background: #4a7a00; }
.badge-pie { background: #c8a000; }
.badge-hbar { background: #6fa300; }
.badge-line { background: #8bc34a; }
.badge-column { background: #b5cc18; color: #333; }
.badge-winloss { background: #7030A0; }
.badge-lollipop { background: #8b0000; }
.badge-default { background: #999; }