diff --git a/dev/ui/example.html b/dev/ui/example.html new file mode 100644 index 000000000..88c824557 --- /dev/null +++ b/dev/ui/example.html @@ -0,0 +1,162 @@ + + + + + Title + + + + +

UI classes demo

+

+ + + + + + + +

+ +
+ +
+ + + + + diff --git a/dev/ui/table.html b/dev/ui/table.html new file mode 100644 index 000000000..99cb5fa06 --- /dev/null +++ b/dev/ui/table.html @@ -0,0 +1,120 @@ + + + + + Table example + + + + + + + +
+ + + + + \ No newline at end of file diff --git a/dev/wig/bedgraph.html b/dev/wig/bedgraph.html index 9ef31a96a..edaf9e98a 100644 --- a/dev/wig/bedgraph.html +++ b/dev/wig/bedgraph.html @@ -19,6 +19,7 @@

bedgraph file

var options = { genome: "hg38", + locus: "muc1", tracks: [ { url: "https://s3.amazonaws.com/igv.org.test/data/bedgraph/ENCFF439GUL.bedgraph.gz", diff --git a/js/feature/wigTrack.js b/js/feature/wigTrack.js index 1b39cb91d..ed72a920a 100755 --- a/js/feature/wigTrack.js +++ b/js/feature/wigTrack.js @@ -8,6 +8,7 @@ import {IGVColor, StringUtils} from "../../node_modules/igv-utils/src/index.js" import $ from "../vendor/jquery-3.3.1.slim.js" import {createCheckbox} from "../igv-icons.js" import {ColorScaleFactory} from "../util/colorScale.js" +import ColorScaleEditor from "../ui/components/colorScaleEditor.js" const DEFAULT_COLOR = 'rgb(150, 150, 150)' @@ -63,28 +64,34 @@ class WigTrack extends TrackBase { } } - if(config.colorScale) { + if (config.colorScale) { this._colorScale = ColorScaleFactory.fromJson(config.colorScale) } // Override default height for heatmaps - if("heatmap" === config.graphType && !config.height) { + if ("heatmap" === config.graphType && !config.height) { this.height = 20 } } - get paintAxis() { - return "heatmap" === this.graphType ? undefined : this._paintAxis - } - async postInit() { const header = await this.getHeader() if (this.disposed) return // This track was removed during async load if (header) this.setTrackProperties(header) } - supportsWholeGenome() { - return this.visibilityWindow === undefined || this.visibilityWindow < 0 + get supportsWholeGenome() { + return !this.config.indexURL && this.config.supportsWholeGenome !== false + } + + get paintAxis() { + // Supply do-nothing implementation for heatmaps + return "heatmap" === this.graphType ? () => { + } : this._paintAxis + } + + get colorScale() { + return this._colorScale } async getFeatures(chr, start, end, bpPerPixel) { @@ -124,18 +131,30 @@ class WigTrack extends TrackBase { menuItemList() { const items = [] - if (this.flipAxis !== undefined) { + if ('heatmap' === this.graphType) { items.push('
') - - function click() { - this.flipAxis = !this.flipAxis - this.trackView.repaintViews() - } - - items.push({label: 'Flip y-axis', click}) + items.push({ + label: 'Set color scale', click: function () { + ColorScaleEditor.open(this.colorScale, this.browser.columnContainer, (colorScale) => { + this._colorScale = colorScale + this.trackView.repaintViews() + }) + } + }) + } else if (this.flipAxis !== undefined) { + items.push('
') + items.push({ + label: 'Flip y-axis', + click: function () { + this.flipAxis = !this.flipAxis + this.trackView.repaintViews() + } + }) } - if(this.featureSource.windowFunctions) { + items.push(...this.graphTypeItems()) + + if (this.featureSource.windowFunctions) { items.push(...this.wigSummarizationItems()) } @@ -165,6 +184,27 @@ class WigTrack extends TrackBase { return menuItems } + graphTypeItems() { + + const graphType = ['bar', 'line', 'points', 'heatmap'] + + const menuItems = [] + menuItems.push('
') + menuItems.push("
Graph type
") + + for (const gt of graphType) { + const object = $(createCheckbox(gt, this.graphType === gt)) + + function clickHandler() { + this.graphType = gt + this.trackView.updateViews() + } + + menuItems.push({object, click: clickHandler}) + } + + return menuItems + } async getHeader() { @@ -188,11 +228,11 @@ class WigTrack extends TrackBase { computeYPixelValueInLogScale(yValue, yScaleFactor) { let maxValue = this.dataRange.max - let minValue = this.dataRange.min + let minValue = this.dataRange.min minValue = (minValue < 0) ? -Math.log10(Math.abs(minValue) + 1) : Math.log10(Math.abs(minValue) + 1) maxValue = (maxValue < 0) ? -Math.log10(Math.abs(maxValue) + 1) : Math.log10(Math.abs(maxValue) + 1) - - yValue = (yValue < 0) ? -Math.log10(Math.abs(yValue) +1) : Math.log10(yValue + 1) + + yValue = (yValue < 0) ? -Math.log10(Math.abs(yValue) + 1) : Math.log10(yValue + 1) return ((this.flipAxis ? (yValue - minValue) : (maxValue - yValue)) * yScaleFactor) } @@ -205,7 +245,7 @@ class WigTrack extends TrackBase { const pixelWidth = options.pixelWidth const pixelHeight = options.pixelHeight - 1 const bpEnd = bpStart + pixelWidth * bpPerPixel + 1 - + const scaleFactor = this.getScaleFactor(this.dataRange.min, this.dataRange.max, pixelHeight, this.logScale) const yScale = (yValue) => this.logScale ? this.computeYPixelValueInLogScale(yValue, scaleFactor) @@ -229,7 +269,7 @@ class WigTrack extends TrackBase { if (f.start > bpEnd) break const x = (f.start - bpStart) / bpPerPixel - if (isNaN(x)) continue + if (Number.isNaN(x)) continue let y = yScale(f.value) @@ -259,14 +299,15 @@ class WigTrack extends TrackBase { } else if (this.graphType === "heatmap") { - if(!this._colorScale) { - throw Error("Graphtype heatmap requires a color scale") + if (!this._colorScale) { + // Create a default color scale. + this._colorScale = this.dataRange.min < 0 && this.dataRange.max > 0 ? + ColorScaleFactory.defaultDivergingScale(this.dataRange.min, 0, this.dataRange.max) : + ColorScaleFactory.defaultGradientScale(this.dataRange.min, this.dataRange.max) } const color = this._colorScale.getColor(f.value) IGVGraphics.fillRect(ctx, x, 0, width, pixelHeight, {fillStyle: color}) - } - - else { + } else { // Default graph type (bar) const height = Math.min(pixelHeight, y - y0) IGVGraphics.fillRect(ctx, x, y0, width, height, {fillStyle: color}) @@ -284,7 +325,7 @@ class WigTrack extends TrackBase { // If the track includes negative values draw a baseline if (this.dataRange.min < 0) { let maxValue = this.dataRange.max - let minValue = this.dataRange.min + let minValue = this.dataRange.min minValue = (this.logScale === true) ? ((minValue < 0) ? -Math.log10(Math.abs(minValue) + 1) : Math.log10(Math.abs(minValue) + 1)) : minValue maxValue = (this.logScale === true) ? ((maxValue < 0) ? -Math.log10(Math.abs(maxValue) + 1) : Math.log10(Math.abs(maxValue) + 1)) : maxValue const ratio = maxValue / (maxValue - minValue) @@ -360,10 +401,6 @@ class WigTrack extends TrackBase { } } - get supportsWholeGenome() { - return !this.config.indexURL && this.config.supportsWholeGenome !== false - } - /** * Return color for feature. * @param feature @@ -375,6 +412,15 @@ class WigTrack extends TrackBase { return (typeof c === "function") ? c(f.value) : c } + + getState() { + const state = super.getState() + if (this._colorScale) { + state.colorScale = this._colorScale.toJson() + } + return state + } + /** * Called when the track is removed. Do any needed cleanup here */ @@ -440,7 +486,7 @@ function summarizeData(features, startBP, bpPerPixel, windowFunction = "mean") { if (!currentBinData || endBin > currentBinData.bin) { - if(currentBinData) { + if (currentBinData) { finishBin(currentBinData) } @@ -454,7 +500,7 @@ function summarizeData(features, startBP, bpPerPixel, windowFunction = "mean") { } } - if(currentBinData) { + if (currentBinData) { finishBin(currentBinData) } diff --git a/js/shoebox/shoeboxTrack.js b/js/shoebox/shoeboxTrack.js index b8554b988..21ba837e6 100755 --- a/js/shoebox/shoeboxTrack.js +++ b/js/shoebox/shoeboxTrack.js @@ -22,7 +22,8 @@ class ShoeboxTrack extends TrackBase { scale: 1.0, visibilityWindow: 10000, supportHiDPI: false, - rowStepSize: 1 // Stepsize for each row in bp for footprint radius + startSize: 4, // Footprint size for the first column of data (first row in track) + stepSize: 2 // Stepsize for each row in bp for footprint radius } constructor(config, browser) { @@ -235,12 +236,12 @@ class ShoeboxTrack extends TrackBase { 'strokeStyle': "black" } - const max = 2 * (this.rowCount + 1) //2x for size in diameter, rather than radius - const min = 0 + const max = this.startSize + (this.rowCount * this.stepSize) + const min = this.startSize const yScale = (max - min) / pixelHeight - const n = 50 - for (let p = n; p <= max; p += n) { + const tickStep = 50 + for (let p = tickStep; p <= max; p += tickStep) { const yp = Math.max(10, pixelHeight - Math.round((p - min) / yScale)) IGVGraphics.strokeLine(ctx, 35, yp , 40, yp , font) if(p > min ) { diff --git a/js/trackBase.js b/js/trackBase.js index a9d8b1241..deb4e0855 100644 --- a/js/trackBase.js +++ b/js/trackBase.js @@ -28,6 +28,7 @@ import {FeatureUtils, FileUtils, StringUtils} from "../node_modules/igv-utils/sr import $ from "./vendor/jquery-3.3.1.slim.js" import {createCheckbox} from "./igv-icons.js" import {findFeatureAfterCenter} from "./feature/featureUtils.js" +import ColorScaleEditor from "./ui/components/colorScaleEditor.js" const DEFAULT_COLOR = 'rgb(150,150,150)' @@ -522,50 +523,47 @@ class TrackBase { menuItems.push('
') - // Data range - let object = $('
') - object.text('Set data range') + // Data range or color scale - function dialogPresentationHandler() { + if ("heatmap" !== this.graphType && this.colorScale) { - if (this.trackView.track.selected) { - this.browser.dataRangeDialog.configure(this.trackView.browser.getSelectedTrackViews()) - } else { - this.browser.dataRangeDialog.configure(this.trackView) + function dialogPresentationHandler() { + + if (this.trackView.track.selected) { + this.browser.dataRangeDialog.configure(this.trackView.browser.getSelectedTrackViews()) + } else { + this.browser.dataRangeDialog.configure(this.trackView) + } + this.browser.dataRangeDialog.present($(this.browser.columnContainer)) } - this.browser.dataRangeDialog.present($(this.browser.columnContainer)) - } - menuItems.push({object, dialog: dialogPresentationHandler}) + menuItems.push({label: 'Set data range', dialog: dialogPresentationHandler}) - if (this.logScale !== undefined) { + if (this.logScale !== undefined) { - object = $(createCheckbox("Log scale", this.logScale)) + function logScaleHandler() { + this.logScale = !this.logScale + this.trackView.repaintViews() + } - function logScaleHandler() { - this.logScale = !this.logScale - this.trackView.repaintViews() + menuItems.push({object: $(createCheckbox("Log scale", this.logScale)), click: logScaleHandler}) } - menuItems.push({object, click: logScaleHandler}) - } - - object = $(createCheckbox("Autoscale", this.autoscale)) + function autoScaleHandler() { + this.autoscaleGroup = undefined + this.autoscale = !this.autoscale + this.browser.updateViews() + } - function autoScaleHandler() { - this.autoscaleGroup = undefined - this.autoscale = !this.autoscale - this.browser.updateViews() + menuItems.push({object: $(createCheckbox("Autoscale", this.autoscale)), click: autoScaleHandler}) } - menuItems.push({object, click: autoScaleHandler}) - return menuItems } - setDataRange({ min, max }) { + setDataRange({min, max}) { - this.dataRange = { min, max } + this.dataRange = {min, max} this.autoscale = false this.autoscaleGroup = undefined this.trackView.repaintViews() diff --git a/js/ui/components/colorScaleEditor.js b/js/ui/components/colorScaleEditor.js new file mode 100644 index 000000000..e06386254 --- /dev/null +++ b/js/ui/components/colorScaleEditor.js @@ -0,0 +1,250 @@ +import * as DOMUtils from "../utils/dom-utils.js" +import Picker from "../../../node_modules/vanilla-picker/dist/vanilla-picker.csp.mjs" +import Dialog from "./dialog.js" +import DOMPurify from "../../../node_modules/dompurify/dist/purify.es.mjs" +import Checkbox from "./checkbox.js" +import {DivergingGradientScale, GradientColorScale} from "../../util/colorScale.js" + + +function paintLegend(legend, newColorScale) { + + const ctx = legend.getContext("2d") + const w = legend.width + const step = (newColorScale.high - newColorScale.low) / w + for (let i = 0; i < w; i++) { + const v = newColorScale.low + i * step + const color = newColorScale.getColor(v) + ctx.fillStyle = color + ctx.fillRect(i, 0, 1, legend.height) + } +} + +/** + * Editor for color scales. Supported types: + * + * 'gradient': {low, high, lowColor, highColor} + * + * 'diverging': {mid, midColor, lowGradientScale, highGradientScale} + * + * + */ +class ColorScaleEditor { + + static open(colorScale, parent, callback) { + + let newColorScale = colorScale.clone() + + const table = document.createElement('table') + + const legend = document.createElement('canvas') + legend.style.height = "20px" + legend.style.width = "100%" + legend.style.marginTop = "10px" + legend.style.border = "1px solid black" + + const minTextbox = new TextBoxRow({ + label: "Min value", + value: newColorScale.low.toString(), + onchange: (v) => { + newColorScale.low = Number.parseFloat(v) + paintLegend(legend, newColorScale) + } + }) + table.append(minTextbox.row) + + const midTextbox = new TextBoxRow({ + label: "Mid value", + value: (newColorScale.mid || newColorScale.low).toString(), + onchange: (v) => { + newColorScale.mid = Number.parseFloat(v) + paintLegend(legend, newColorScale) + } + }) + table.append(midTextbox.row) + + const maxTextbox = new TextBoxRow({ + label: "Max value", + value: newColorScale.high.toString(), + onchange: (v) => { + newColorScale.high = Number.parseFloat(v) + paintLegend(legend, newColorScale) + } + }) + table.append(maxTextbox.row) + + + const colorElem = new ColorPickerRow({ + label: "Min color", + value: newColorScale.lowColor, + onchange: (v) => { + newColorScale.lowColor = v + paintLegend(legend, newColorScale) + } + }) + table.append(colorElem.row) + + const midColorElem = new ColorPickerRow({ + label: "Mid color", + value: newColorScale.midColor || newColorScale.lowColor, + onchange: (v) => { + newColorScale.midColor = v + paintLegend(legend, newColorScale) + } + }) + table.append(midColorElem.row) + + const highColorElem = new ColorPickerRow({ + label: "Max color", + value: newColorScale.highColor, + onchange: (v) => { + newColorScale.maxColor = v + paintLegend(legend, newColorScale) + } + }) + table.append(highColorElem.row) + + const divergingCheckbox = new Checkbox({ + selected: "diverging" === colorScale.type, + label: "Diverging Scale", + onchange: (diverging) => { + if (diverging) { + // Converting from gradient to diverting + newColorScale.mid = newColorScale.low < 0 && newColorScale.high > 0 ? 0 : (newColorScale.low + newColorScale.high) / 2 + newColorScale.midColor = "rgb(255,255,255)" + newColorScale = new DivergingGradientScale(newColorScale) + + midTextbox.value = newColorScale.mid + midTextbox.show() + + midColorElem.value = newColorScale.midColor + midColorElem.show() + + paintLegend(legend, newColorScale) + } else { + + // Converting from diverging to gradient + newColorScale = new GradientColorScale(newColorScale) + midTextbox.hide() + midColorElem.hide() + paintLegend(legend, newColorScale) + } + } + }) + divergingCheckbox.elem.style.marginBottom = "20px" + + if('diverging' !== colorScale.type) { + midTextbox.hide() + midColorElem.hide() + } + + const panel = document.createElement('div') + panel.appendChild(divergingCheckbox.elem) + panel.appendChild(table) + panel.appendChild(legend) + + const okHandler = () => { + if (callback) { + callback(newColorScale) + } + + } + + const config = { + parent, // label: 'Multi-select', + content: {elem: panel}, okHandler + } + const dialog = new Dialog(config) + parent.append(dialog.elem) + DOMUtils.show(dialog.elem) + + paintLegend(legend, newColorScale) + + } + +} + +class LabeledButtonRow { + constructor({label, value, onchange}) { + + this.row = document.createElement('tr') + const cell = document.createElement('td') + this.row.appendChild(cell) + + const div = document.createElement('div') + div.innerHTML = label + cell.appendChild(div) + } + + hide() { + this.row.style.display = 'none' + } + + show() { + this.row.style.display = 'table-row' + } +} + +class TextBoxRow extends LabeledButtonRow { + + constructor({label, value, onchange}) { + super({label, value, onchange}) + + const cell2 = document.createElement('td') + this.row.appendChild(cell2) + this.input = document.createElement('input') + + value = value || "0" + this.input.value = DOMPurify.sanitize(value) + + cell2.appendChild(this.input) + + if (onchange) { + this.input.addEventListener('change', (e) => onchange(this.input.value)) + } + } + + get value() { + return this.input.value + } + + set value(v) { + this.input.value = v + } +} + +class ColorPickerRow extends LabeledButtonRow { + + constructor({label, value, onchange}) { + super({label, value, onchange}) + + const cell2 = document.createElement('td') + this.row.appendChild(cell2) + const colorButton = document.createElement('div') + cell2.appendChild(colorButton) + colorButton.style.width = "20px" + colorButton.style.height = "20px" + colorButton.style.border = "1px solid black" + this.colorButton = colorButton + + value = value || "white" + colorButton.style.background = value + + const picker = new Picker(colorButton) + picker.setOptions({ + alpha: false, color: value + }) + + picker.onDone = (color) => { + colorButton.style.background = color.rgbString + if (onchange) { + onchange(color.rgbString) + } + } + } + + set value(c) { + this.colorButton.style.background = c + } +} + +export default ColorScaleEditor diff --git a/js/ui/components/textbox.js b/js/ui/components/textbox.js index 9de3f2f50..a5dbc1469 100644 --- a/js/ui/components/textbox.js +++ b/js/ui/components/textbox.js @@ -1,5 +1,5 @@ import * as DOMUtils from "../utils/dom-utils.js" -import DOMPurify from "../../node_modules/dompurify/dist/purify.es.mjs" +import DOMPurify from "../../../node_modules/dompurify/dist/purify.es.mjs" class Textbox { diff --git a/js/ui/menuUtils.js b/js/ui/menuUtils.js index c7db4c4d6..93dabd666 100644 --- a/js/ui/menuUtils.js +++ b/js/ui/menuUtils.js @@ -346,7 +346,7 @@ function getTrackLabelText(track) { } function canShowColorPicker(track) { - return undefined === track.type || colorPickerTrackTypeSet.has(track.type) + return undefined === track.type || (colorPickerTrackTypeSet.has(track.type) && 'heatmap' !== track.graphType) } function didSelectSingleTrackType(types) { diff --git a/js/ui/utils/dom-utils.js b/js/ui/utils/dom-utils.js index 1c687aeef..36f18ffce 100644 --- a/js/ui/utils/dom-utils.js +++ b/js/ui/utils/dom-utils.js @@ -28,11 +28,11 @@ function hide(elem) { } function show(elem) { - const currentDisplay = getComputedStyle(elem).display; - if (currentDisplay === "none") { + //const currentDisplay = getComputedStyle(elem).display; + //if (currentDisplay === "none") { const d = elem._initialDisplay || "block"; elem.style.display = d; - } + // } } function empty(elem) { diff --git a/js/util/colorScale.js b/js/util/colorScale.js index 51fe5a9a6..a8be10120 100644 --- a/js/util/colorScale.js +++ b/js/util/colorScale.js @@ -4,15 +4,38 @@ import {IGVColor} from "../../node_modules/igv-utils/src/index.js" const ColorScaleFactory = { fromJson: (obj) => { - - switch(obj.type) { + switch (obj.type) { case 'gradient': return new GradientColorScale(obj) case 'doubleGradient': - return new DoubleGradientScale(obj) + case 'diverging': + return new DivergingGradientScale(obj) default: throw Error("Unknown color scale type: " + obj) } + }, + + defaultGradientScale: function (low, high) { + + return new GradientColorScale({ + "type": "doubleGradient", + "low": low, + "high": high, + "lowColor": "rgb(46,56,183)", + "highColor": "rgb(164,0,30)" + }) + }, + + defaultDivergingScale: function (low, mid, high) { + return new DivergingGradientScale({ + "type": "doubleGradient", + "low": 0, + "mid": 0.25, + "high": 0.5, + "lowColor": "rgb(46,56,183)", + "midColor": "white", + "highColor": "rgb(164,0,30)" + }) } } @@ -41,30 +64,39 @@ class BinnedColorScale { } } -/** - * - * @param scale - object with the following properties - * low - * lowR - * lowG - * lowB - * high - * highR - * highG - * highB - * - * @constructor - */ + class GradientColorScale { constructor({low, high, lowColor, highColor}) { + this.type = 'gradient' + this.setProperties({low, high, lowColor, highColor}) + } + setProperties({low, high, lowColor, highColor}) { + this.type = 'gradient' this.low = low this.high = high - this.diff = high - low - this.lowColor = lowColor - this.highColor = highColor - this.lowComponents = IGVColor.rgbComponents(this.lowColor) - this.highComponents = IGVColor.rgbComponents(this.highColor) + this._lowColor = lowColor + this._highColor = highColor + this.lowComponents = IGVColor.rgbComponents(lowColor) + this.highComponents = IGVColor.rgbComponents(highColor) + } + + get lowColor() { + return this._lowColor + } + + set lowColor(c) { + this._lowColor = c + this.lowComponents = IGVColor.rgbComponents(c) + } + + get highColor() { + return this._highColor + } + + set highColor(c) { + this._highColor = c + this.highComponents = IGVColor.rgbComponents(c) } getColor(value) { @@ -72,7 +104,7 @@ class GradientColorScale { if (value <= this.low) return this.lowColor else if (value >= this.high) return this.highColor - const frac = (value - this.low) / this.diff + const frac = (value - this.low) / (this.high - this.low) const r = Math.floor(this.lowComponents[0] + frac * (this.highComponents[0] - this.lowComponents[0])) const g = Math.floor(this.lowComponents[1] + frac * (this.highComponents[1] - this.lowComponents[1])) const b = Math.floor(this.lowComponents[2] + frac * (this.highComponents[2] - this.lowComponents[2])) @@ -86,6 +118,7 @@ class GradientColorScale { */ toJson() { return { + type: this.type, low: this.low, high: this.high, lowColor: this.lowColor, @@ -93,42 +126,113 @@ class GradientColorScale { } } + clone() { + return new GradientColorScale(this.toJson()) + } + } -class DoubleGradientScale { +class DivergingGradientScale { constructor({lowColor, midColor, highColor, low, mid, high}) { - this.mid = mid - this.midColor = midColor - this.lowGradientScale = new GradientColorScale({lowColor, highColor: midColor, low, high: mid}) - this.highGradientScale = new GradientColorScale({lowColor: midColor, highColor, low: mid, high}) + this.type = 'diverging' + this.setProperties({lowColor, midColor, highColor, low, mid, high}) + } + + setProperties({lowColor, midColor, highColor, low, mid, high}) { + + this.lowGradientScale = new GradientColorScale({ + lowColor: lowColor, + highColor: midColor, + low: low, + high: mid + }) + this.highGradientScale = new GradientColorScale({ + lowColor: midColor, + highColor: highColor, + low: mid, + high: high + }) } getColor(value) { if (value < this.mid) { return this.lowGradientScale.getColor(value) - } else if(value > this.mid) { - return this.highGradientScale.getColor(value) } else { - return this.midColor + return this.highGradientScale.getColor(value) } } + get low() { + return this.lowGradientScale.low + } + + set low(v) { + this.lowGradientScale.low = v + } + + get high() { + return this.highGradientScale.high + } + + set high(v) { + this.highGradientScale.high = v + } + + get mid() { + return this.lowGradientScale.high + } + + set mid(v) { + this.lowGradientScale.high = v + this.highGradientScale.low = v + } + + get lowColor() { + return this.lowGradientScale.lowColor + } + + set lowColor(c) { + this.lowGradientScale.lowColor = c + } + + get highColor() { + return this.highGradientScale.highColor + } + + set highColor(c) { + this.highGradientScale.highColor = c + } + + get midColor() { + return this.lowGradientScale.highColor + } + + set midColor(c) { + this.lowGradientScale.highColor = c + this.highGradientScale.lowColor = c + } + /** * Return a simple json-like object, not a literaly json string - * @returns {{high, low, highColor, lowColor}} + * @returns {{high, mid, low, highColor, midColor, lowColor}} */ toJson() { return { - low: this.low, + type: this.type, + low: this.lowGradientScale.low, mid: this.mid, - high: this.high, - lowColor: this.lowColor, + high: this.highGradientScale.high, + lowColor: this.lowGradientScale.lowColor, midColor: this.midColor, - highColor: this.highColor + highColor: this.highGradientScale.highColor } } + + clone() { + return new DivergingGradientScale(this.toJson()) + } } class ConstantColorScale { @@ -142,4 +246,4 @@ class ConstantColorScale { } -export {BinnedColorScale, GradientColorScale, ConstantColorScale, DoubleGradientScale, ColorScaleFactory} +export {BinnedColorScale, GradientColorScale, ConstantColorScale, DivergingGradientScale, ColorScaleFactory}