1589 lines
59 KiB
JavaScript
1589 lines
59 KiB
JavaScript
Sk.jsplotlib = Sk.jsplotlib || {};
|
|
|
|
// Skulpt translation
|
|
let $builtinmodule = function (name) {
|
|
let mod = {__name__: "matplotlib.pyplot"};
|
|
|
|
const STRING_COLOR = new Sk.builtin.str("color");
|
|
const STRING_COLORS = new Sk.builtin.str("colors");
|
|
const STRING_DATA = new Sk.builtin.str("data");
|
|
const STRING_LABEL = new Sk.builtin.str("label");
|
|
const STRING_ALPHA = new Sk.builtin.str("alpha");
|
|
const STRING_DASH_CAPSTYLE = new Sk.builtin.str("dash_capstyle");
|
|
const STRING_DASH_JOINSTYLE = new Sk.builtin.str("dash_joinstyle");
|
|
const STRING_FILLSTYLE = new Sk.builtin.str("fillstyle");
|
|
const STRING_LINEWIDTH = new Sk.builtin.str("linewidth");
|
|
const STRING_MARKER = new Sk.builtin.str("marker");
|
|
const STRING_LINESTYLE = new Sk.builtin.str("linestyle");
|
|
const STRING_LINESTYLES = new Sk.builtin.str("linestyles");
|
|
const STRING_BINS = new Sk.builtin.str("bins");
|
|
const STRING_ALIGN = new Sk.builtin.str("align");
|
|
const STRING_DOT_LIMIT = new Sk.builtin.str("dot_limit");
|
|
const STRING_S = new Sk.builtin.str("s");
|
|
const STRING_C = new Sk.builtin.str("c");
|
|
const STRING_LINEWIDTHS = new Sk.builtin.str("linewidths");
|
|
const STRING_EDGECOLORS = new Sk.builtin.str("edgecolors");
|
|
const STRING_WIDTH = new Sk.builtin.str("width");
|
|
const STRING_TICK_LABEL = new Sk.builtin.str("tick_label");
|
|
const STRING_ROTATION = new Sk.builtin.str("rotation");
|
|
|
|
const DEFAULT_PLOT_PADDING = .5;
|
|
|
|
|
|
/*if (Sk.MatPlotLibGraphics === undefined) {
|
|
throw new Sk.builtin.NameError("Can not resolve drawing area. Sk.MatPlotLibGraphics is undefined!");
|
|
}*/
|
|
function getConsole() {
|
|
return Sk.MatPlotLibGraphics.target;
|
|
}
|
|
// 重写获取画布宽度方法
|
|
function getWidth() {
|
|
return Sk.MatPlotLibGraphics.width;
|
|
}
|
|
|
|
// 重写获取画布长度方法
|
|
function getHeight() {
|
|
return Sk.MatPlotLibGraphics.height;
|
|
}
|
|
// Unique ID generator for charts
|
|
let chartCounter = 0;
|
|
|
|
function makePlot(type, data, style, label) {
|
|
return {
|
|
type: type,
|
|
data: data,
|
|
style: style,
|
|
label: label
|
|
};
|
|
}
|
|
|
|
function makeChart() {
|
|
let margin = {top: 20, right: 30, bottom: 50, left: 40};
|
|
let chartIdNumber = chartCounter++;
|
|
return {
|
|
plots: [],
|
|
labels: {
|
|
title: "",
|
|
"x-axis": "",
|
|
"y-axis": ""
|
|
},
|
|
extents: {
|
|
xMin: null,
|
|
xMax: null,
|
|
yMin: null,
|
|
yMax: null
|
|
},
|
|
ticks: {
|
|
x: {},
|
|
y: {},
|
|
xRotate: "horizontal",
|
|
yRotate: "horizontal",
|
|
xEstimate: new Set(),
|
|
yEstimate: new Set()
|
|
},
|
|
margin: margin,
|
|
width: getWidth() - margin.left - margin.right,
|
|
height: getWidth() - margin.top - margin.bottom,
|
|
idNumber: chartIdNumber,
|
|
id: "chart" + chartIdNumber,
|
|
colorCycle: 0
|
|
};
|
|
}
|
|
|
|
function updateExtent(chart, attr, array, padding) {
|
|
if (padding === undefined) {
|
|
padding = 0;
|
|
}
|
|
if (chart.extents[attr + "Min"] === null) {
|
|
chart.extents[attr + "Min"] = d3.min(array)-padding;
|
|
} else {
|
|
chart.extents[attr + "Min"] = Math.min(d3.min(array)-padding, chart.extents[attr + "Min"]);
|
|
}
|
|
if (chart.extents[attr + "Max"] === null) {
|
|
chart.extents[attr + "Max"] = d3.max(array)+padding;
|
|
} else {
|
|
chart.extents[attr + "Max"] = Math.max(d3.max(array)+padding, chart.extents[attr + "Max"]);
|
|
}
|
|
}
|
|
|
|
let chart = null;
|
|
function getChart() {
|
|
if (!chart) {
|
|
chart = makeChart();
|
|
}
|
|
return chart;
|
|
}
|
|
|
|
function resetChart() {
|
|
chart = makeChart();
|
|
}
|
|
|
|
function parseFormat(styleString, chart, kwargs, defaultLinestyle, defaultMarker) {
|
|
// TODO: Handle ls, lw, and other abbreviated keyword parameters
|
|
let format = {
|
|
alpha: getKeywordParameter(kwargs, STRING_ALPHA, null),
|
|
dash_capstyle: getKeywordParameter(kwargs, STRING_DASH_CAPSTYLE, "butt"),
|
|
dash_joinstyle: getKeywordParameter(kwargs, STRING_DASH_JOINSTYLE, "miter"),
|
|
fillstyle: getKeywordParameter(kwargs, STRING_FILLSTYLE, "full"),
|
|
linewidth: getKeywordParameter(kwargs, STRING_LINEWIDTH, 1),
|
|
};
|
|
if (styleString) {
|
|
let ftmTuple = Sk.jsplotlib._process_plot_format(styleString);
|
|
format["linestyle"] = getKeywordParameter(kwargs, STRING_LINESTYLE, ftmTuple.linestyle);
|
|
format["marker"] = getKeywordParameter(kwargs, STRING_MARKER, Sk.jsplotlib.parse_marker(ftmTuple.marker));
|
|
format["color"] = getKeywordParameter(kwargs, STRING_COLOR, Sk.jsplotlib.color_to_hex(ftmTuple.color));
|
|
} else {
|
|
let cycle = Sk.jsplotlib.rc["axes.color_cycle"];
|
|
format["linestyle"] = getKeywordParameter(kwargs, STRING_LINESTYLE, defaultLinestyle);
|
|
format["marker"] = getKeywordParameter(kwargs, STRING_MARKER, defaultMarker);
|
|
format["color"] = getKeywordParameter(kwargs, STRING_COLOR, Sk.jsplotlib.color_to_hex(cycle[chart.colorCycle % cycle.length]));
|
|
chart.colorCycle += 1;
|
|
}
|
|
return format;
|
|
}
|
|
|
|
function getIndices(values) {
|
|
return values.map((value, index) => index);
|
|
}
|
|
|
|
function chompPlotArgs(args, data) {
|
|
if (data !== null) {
|
|
if (args.length >= 2) {
|
|
throw new Sk.builtin.ValueError("Must provide at least 2 arguments when plotting with 'data' keyword");
|
|
}
|
|
let xAttr = args[0];
|
|
let yAttr = args[1];
|
|
let xs = [], ys = [];
|
|
const values = Sk.misceval.arrayFromIterable(data);
|
|
for (let i = 0; i < values.length; i++) {
|
|
if (values[i].sq$contains(xAttr)) {
|
|
xs.push(values[i].mp$subscript(xAttr));
|
|
ys.push(values[i].mp$subscript(yAttr));
|
|
} else {
|
|
throw new Sk.builtin.ValueError(`Item at index ${i} is missing expected attribute.`);
|
|
}
|
|
}
|
|
return [xs, ys, args[2]];
|
|
} else {
|
|
// x, y, style
|
|
if (args.length >= 3) {
|
|
if (Sk.builtin.checkString(args[2])) {
|
|
return [[args[0], args[1], args[2]].map(Sk.ffi.remapToJs)].concat(chompPlotArgs(args.slice(3), data));
|
|
}
|
|
}
|
|
if (args.length >= 2) {
|
|
let ys = Sk.ffi.remapToJs(args[0]);
|
|
if (Sk.builtin.checkString(args[1])) {
|
|
// y, style
|
|
let xs = getIndices(ys);
|
|
let style = Sk.ffi.remapToJs(args[1]);
|
|
return [[xs, ys, style]].concat(chompPlotArgs(args.slice(2), data));
|
|
} else {
|
|
// x, y
|
|
let xs = ys;
|
|
ys = Sk.ffi.remapToJs(args[1]);
|
|
return [[xs, ys, ""]].concat(chompPlotArgs(args.slice(2), data));
|
|
}
|
|
}
|
|
if (args.length >= 1) {
|
|
// y
|
|
let ys = Sk.ffi.remapToJs(args[0]);
|
|
let xs = getIndices(ys);
|
|
return [[xs, ys, ""]].concat(chompPlotArgs(args.slice(1), data));
|
|
}
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function getKeywordParameter(kwargs, key, otherwise) {
|
|
if (kwargs.sq$contains(key)) {
|
|
return Sk.ffi.remapToJs(kwargs.mp$subscript(key));
|
|
} else {
|
|
return otherwise;
|
|
}
|
|
}
|
|
|
|
function updateTickEstimates(chart, xValues, yValues) {
|
|
xValues.forEach(value => chart.ticks.xEstimate.add(value));
|
|
yValues.forEach(value => chart.ticks.yEstimate.add(value));
|
|
}
|
|
|
|
// Main plotting function
|
|
let plot_f = function (kwa) {
|
|
// Parse arguments
|
|
Sk.builtin.pyCheckArgs("plot", arguments, 1, Infinity, true, false);
|
|
let args = Array.prototype.slice.call(arguments, 1);
|
|
let kwargs = new Sk.builtins.dict(kwa); // Get argument as object
|
|
let data = getKeywordParameter(kwargs, STRING_DATA, null);
|
|
let label = getKeywordParameter(kwargs, STRING_LABEL, null);
|
|
let chart = getChart();
|
|
let plotData = chompPlotArgs(args, data);
|
|
for (let i=0; i<plotData.length; i+= 1) {
|
|
let plotDatum = plotData[i];
|
|
let zippedData = d3.zip(plotDatum[0], plotDatum[1]).map(value => {return {x: value[0], y: value[1]};});
|
|
let style = parseFormat(plotDatum[2], chart, kwargs, "-", "");
|
|
let plot = makePlot("line", zippedData, style, label);
|
|
chart.plots.push(plot);
|
|
updateExtent(chart, "x", plotDatum[0], DEFAULT_PLOT_PADDING);
|
|
updateExtent(chart, "y", plotDatum[1], DEFAULT_PLOT_PADDING);
|
|
updateTickEstimates(chart, plotDatum[0], plotDatum[1]);
|
|
}
|
|
};
|
|
plot_f.co_kwargs = true;
|
|
mod.plot = new Sk.builtin.func(plot_f);
|
|
|
|
|
|
let hist_f = function (kwa) {
|
|
// Parse arguments
|
|
Sk.builtin.pyCheckArgs("hist", arguments, 1, Infinity, true, false);
|
|
let args = Array.prototype.slice.call(arguments, 1);
|
|
let kwargs = new Sk.builtins.dict(kwa); // Get argument as object
|
|
|
|
// Parse different args combinations
|
|
let plotData = Sk.ffi.remapToJs(args[0]);
|
|
if (plotData.length > 0) {
|
|
if (!Array.isArray(plotData[0])) {
|
|
plotData = [plotData];
|
|
}
|
|
}
|
|
|
|
let label = getKeywordParameter(kwargs, STRING_LABEL, null);
|
|
let chart = getChart();
|
|
for (let i=0; i<plotData.length; i+= 1) {
|
|
let plotDatum = plotData[i];
|
|
let style = parseFormat(null, chart, kwargs, "", "");
|
|
let plot = makePlot("hist", plotDatum, style, label);
|
|
plot.bins = getKeywordParameter(kwargs, STRING_BINS, null);
|
|
plot.align = getKeywordParameter(kwargs, STRING_ALIGN, "mid");
|
|
let estimatedBins = plotDatum;
|
|
if (Array.isArray(plot.bins)) {
|
|
let max = d3.max(plot.bins), min = d3.min(plot.bins);
|
|
plot.data = plotDatum = plot.data.map((value) => Math.max(Math.min(value, max), min));
|
|
estimatedBins = plot.bins;
|
|
}
|
|
chart.plots.push(plot);
|
|
updateExtent(chart, "x", plotDatum, DEFAULT_PLOT_PADDING);
|
|
updateTickEstimates(chart, estimatedBins, d3.range(plotDatum.length));
|
|
}
|
|
// Ensure that the axis line is always the middle!
|
|
updateExtent(chart, "y", [0]);
|
|
};
|
|
hist_f.co_kwargs = true;
|
|
mod.hist = new Sk.builtin.func(hist_f);
|
|
|
|
let scatter_f = function (kwa) {
|
|
// Parse arguments
|
|
Sk.builtin.pyCheckArgs("scatter", arguments, 1, Infinity, true, false);
|
|
let args = Array.prototype.slice.call(arguments, 1);
|
|
let kwargs = new Sk.builtins.dict(kwa); // Get argument as object
|
|
|
|
let data = getKeywordParameter(kwargs, STRING_DATA, null);
|
|
let label = getKeywordParameter(kwargs, STRING_LABEL, null);
|
|
|
|
let plotData = chompPlotArgs(args, data);
|
|
|
|
// Special dot_limit parameter to prevent crashing from too many dots in browser
|
|
let dotLimit = getKeywordParameter(kwargs, STRING_DOT_LIMIT, 256);
|
|
if (plotData[0] && plotData[0].length > dotLimit) {
|
|
let xSampled = [], ySampled = [];
|
|
let LENGTH = plotData[0].length, RATE = LENGTH / dotLimit;
|
|
for (let i = 0; i < dotLimit; i += 1) {
|
|
let index = Math.floor((i + Math.random()) * RATE);
|
|
xSampled.push(plotData[0][index]);
|
|
ySampled.push(plotData[1][index]);
|
|
}
|
|
plotData[0] = xSampled;
|
|
plotData[1] = ySampled;
|
|
}
|
|
|
|
|
|
let chart = getChart();
|
|
for (let i=0; i<plotData.length; i+= 1) {
|
|
let plotDatum = plotData[i];
|
|
let style = parseFormat(plotDatum[2], chart, kwargs, " ", "o");
|
|
let zippedData = d3.zip(plotDatum[0], plotDatum[1]).map(value => {return {x: value[0], y: value[1]};});
|
|
let plot = makePlot("scatter", zippedData, style, label);
|
|
plot.sizes = getKeywordParameter(kwargs, STRING_S, null);
|
|
plot.colors = getKeywordParameter(kwargs, STRING_C, null);
|
|
plot.linewidths = getKeywordParameter(kwargs, STRING_LINEWIDTHS, 1.5);
|
|
plot.edgecolors = getKeywordParameter(kwargs, STRING_EDGECOLORS, "face");
|
|
chart.plots.push(plot);
|
|
updateExtent(chart, "x", plotDatum[0], DEFAULT_PLOT_PADDING);
|
|
updateExtent(chart, "y", plotDatum[1], DEFAULT_PLOT_PADDING);
|
|
updateTickEstimates(chart, plotDatum[0], plotDatum[1]);
|
|
}
|
|
};
|
|
scatter_f.co_kwargs = true;
|
|
mod.scatter = new Sk.builtin.func(scatter_f);
|
|
|
|
let bar_f = function (kwa) {
|
|
// Parse arguments
|
|
Sk.builtin.pyCheckArgs("bar", arguments, 1, Infinity, true, false);
|
|
let args = Array.prototype.slice.call(arguments, 1);
|
|
let kwargs = new Sk.builtins.dict(kwa); // Get argument as object
|
|
|
|
let data = getKeywordParameter(kwargs, STRING_DATA, null);
|
|
let label = getKeywordParameter(kwargs, STRING_LABEL, null);
|
|
let tickLabels = getKeywordParameter(kwargs, STRING_TICK_LABEL, []);
|
|
|
|
let plotData = chompPlotArgs(args, data);
|
|
|
|
let chart = getChart();
|
|
for (let i=0; i<plotData.length; i+= 1) {
|
|
let plotDatum = plotData[i];
|
|
let zippedData = d3.zip(plotDatum[0], plotDatum[1]).map(value => {return {x: value[0], y: value[1]};});
|
|
let style = parseFormat(null, chart, kwargs, "", "");
|
|
let plot = makePlot("bar", zippedData, style, label);
|
|
plot.width = getKeywordParameter(kwargs, STRING_WIDTH, 0.8);
|
|
plot.align = getKeywordParameter(kwargs, STRING_ALIGN, "center");
|
|
for (let x=0; x<Math.min(tickLabels.length, plotDatum[0].length); x+=1) {
|
|
chart.ticks.x[plotDatum[0][x]] = tickLabels[x];
|
|
}
|
|
chart.plots.push(plot);
|
|
let lowestX = d3.min(plotDatum[0])-(plot.align === "center" ? 1 : 0), lowestY = d3.max(plotDatum[0])+1;
|
|
updateExtent(chart, "x", plotDatum[0].concat([lowestX, lowestY])); // Account for width
|
|
updateExtent(chart, "y", plotDatum[1]);
|
|
updateTickEstimates(chart, plotDatum[0].concat([lowestX, lowestY]), plotDatum[1]);
|
|
}
|
|
// Ensure that the axis line is always the middle!
|
|
updateExtent(chart, "y", [-.1]);
|
|
};
|
|
bar_f.co_kwargs = true;
|
|
mod.bar = new Sk.builtin.func(bar_f);
|
|
|
|
let boxplot_f = function (kwa) {
|
|
// Parse arguments
|
|
Sk.builtin.pyCheckArgs("boxplot", arguments, 1, Infinity, true, false);
|
|
let args = Array.prototype.slice.call(arguments, 1);
|
|
let kwargs = new Sk.builtins.dict(kwa); // Get argument as object
|
|
|
|
let plotData = Sk.ffi.remapToJs(args[0]);
|
|
let label = getKeywordParameter(kwargs, STRING_LABEL, null);
|
|
let chart = getChart();
|
|
let dataSorted = plotData.sort(d3.ascending);
|
|
let q1 = d3.quantile(dataSorted, .25);
|
|
let median = d3.quantile(dataSorted, .5);
|
|
let q3 = d3.quantile(dataSorted, .75);
|
|
let interQuantileRange = q3 - q1;
|
|
let min = d3.min(plotData);
|
|
let max = d3.max(plotData);
|
|
let data = {x: chart.plots.length+1, y: [min, q1, median, q3, max]};
|
|
let style = parseFormat(null, chart, kwargs, "", "");
|
|
let plot = makePlot("boxplot", data, style, label);
|
|
chart.plots.push(plot);
|
|
updateExtent(chart, "x", [data.x-1, data.x, data.x+1]); // Account for width
|
|
updateExtent(chart, "y", data.y);
|
|
updateExtent(chart, "y", [-.1]);
|
|
updateTickEstimates(chart, [data.x-1, data.x+1], data.y);
|
|
};
|
|
boxplot_f.co_kwargs = true;
|
|
mod.boxplot = new Sk.builtin.func(boxplot_f);
|
|
|
|
function getLinesData(args) {
|
|
args = args.slice(0, 3).map(Sk.ffi.remapToJs);
|
|
let length = d3.max(args.filter(Array.isArray).map(a => a.length)) || 1;
|
|
args = args.map(a => Array.isArray(a) ? a : Array(length).fill(a));
|
|
return d3.zip(args[0], args[1], args[2]);
|
|
}
|
|
|
|
let hlines_f = function (kwa) {
|
|
// Parse arguments
|
|
Sk.builtin.pyCheckArgs("hlines", arguments, 1, Infinity, true, false);
|
|
let args = Array.prototype.slice.call(arguments, 1);
|
|
let kwargs = new Sk.builtins.dict(kwa); // Get argument as object
|
|
|
|
let data = getKeywordParameter(kwargs, STRING_DATA, null);
|
|
let label = getKeywordParameter(kwargs, STRING_LABEL, null);
|
|
let colors = getKeywordParameter(kwargs, STRING_COLORS, null);
|
|
let linestyles = getKeywordParameter(kwargs, STRING_LINESTYLES, null);
|
|
|
|
let plotData = getLinesData(args);
|
|
|
|
let chart = getChart();
|
|
let style = parseFormat(null, chart, kwargs, "", "");
|
|
for (let i=0; i<plotData.length; i+= 1) {
|
|
let plotDatum = plotData[i];
|
|
let zippedData = {x1: plotDatum[1], x2: plotDatum[2], y1: plotDatum[0], y2: plotDatum[0]};
|
|
let plot = makePlot("one_line", zippedData, style, label);
|
|
plot.color = Array.isArray(colors) ? colors[i] : plot.color;
|
|
plot.linestyle = linestyles === null ? plot.linestyle : linestyles;
|
|
chart.plots.push(plot);
|
|
updateExtent(chart, "x", [zippedData.x1, zippedData.x2]); // Account for width
|
|
updateExtent(chart, "y", [zippedData.y1, zippedData.y2]);
|
|
updateTickEstimates(chart, [zippedData.x1, zippedData.x2], [zippedData.y1, zippedData.y2]);
|
|
}
|
|
};
|
|
hlines_f.co_kwargs = true;
|
|
mod.hlines = new Sk.builtin.func(hlines_f);
|
|
|
|
let vlines_f = function (kwa) {
|
|
// Parse arguments
|
|
Sk.builtin.pyCheckArgs("vlines", arguments, 1, Infinity, true, false);
|
|
let args = Array.prototype.slice.call(arguments, 1);
|
|
let kwargs = new Sk.builtins.dict(kwa); // Get argument as object
|
|
|
|
let data = getKeywordParameter(kwargs, STRING_DATA, null);
|
|
let label = getKeywordParameter(kwargs, STRING_LABEL, null);
|
|
let colors = getKeywordParameter(kwargs, STRING_COLORS, null);
|
|
let linestyles = getKeywordParameter(kwargs, STRING_LINESTYLES, null);
|
|
|
|
let plotData = getLinesData(args);
|
|
|
|
let chart = getChart();
|
|
let style = parseFormat(null, chart, kwargs, "", "");
|
|
for (let i=0; i<plotData.length; i+= 1) {
|
|
let plotDatum = plotData[i];
|
|
let zippedData = {x1: plotDatum[0], x2: plotDatum[0], y1: plotDatum[1], y2: plotDatum[2]};
|
|
let plot = makePlot("one_line", zippedData, style, label);
|
|
plot.color = Array.isArray(colors) ? colors[i] : plot.color;
|
|
plot.linestyle = linestyles === null ? plot.linestyle : linestyles;
|
|
chart.plots.push(plot);
|
|
updateExtent(chart, "x", [zippedData.x1, zippedData.x2]); // Account for width
|
|
updateExtent(chart, "y", [zippedData.y1, zippedData.y2]);
|
|
updateTickEstimates(chart, [zippedData.x1, zippedData.x2], [zippedData.y1, zippedData.y2]);
|
|
}
|
|
};
|
|
vlines_f.co_kwargs = true;
|
|
mod.vlines = new Sk.builtin.func(vlines_f);
|
|
|
|
function getRotation(chart, dimension) {
|
|
let amount = chart.ticks[dimension+"Rotate"];
|
|
switch (amount) {
|
|
case "horizontal": return "rotate(0)";
|
|
case "vertical": return "rotate(275)";
|
|
default: return "rotate("+(-amount)+")";
|
|
}
|
|
}
|
|
|
|
function attachCanvasToChart(chart, yAxisBuffer) {
|
|
let outputTarget = getConsole(); //Sk.console.plot(chart);
|
|
chart.svg = d3.select(outputTarget).append("div").append("svg");
|
|
chart.svg.attr("class", "chart");
|
|
chart.svg.attr("width", getWidth());
|
|
chart.svg.attr("height", getHeight());
|
|
chart.svg.attr("chartCount", chart.idNumber);
|
|
|
|
let translation = "translate(" + (chart.margin.left + yAxisBuffer) + "," + chart.margin.top + ")";
|
|
chart.canvas = chart.svg.append("g")
|
|
.attr("transform", translation);
|
|
// X-axis
|
|
chart.canvas.append("g")
|
|
.attr("class", "x axis")
|
|
.attr("transform", "translate(0," + chart.height + ")")
|
|
.call(chart.xAxis);
|
|
// Y-axis
|
|
chart.canvas.append("g")
|
|
.attr("class", "y axis")
|
|
.call(chart.yAxis);
|
|
// X-axis ticks
|
|
let xticks = chart.canvas.select(".x.axis")
|
|
.selectAll("text")
|
|
.style("font-size", "12px");
|
|
if (chart.ticks.xRotate !== "horizontal") {
|
|
xticks.style("text-anchor", "end").attr("transform", getRotation(chart, "x"));
|
|
}
|
|
// Y-axis ticks
|
|
let yticks = chart.canvas.select(".y.axis")
|
|
.selectAll("text")
|
|
.style("font-size", "12px");
|
|
//.style("text-anchor", "start")
|
|
if (chart.ticks.yRotate !== "horizontal") {
|
|
yticks.attr("transform", getRotation(chart, "y"));
|
|
}
|
|
|
|
translation = "translate(" + ((chart.width - yAxisBuffer) / 2) + " ," + (chart.height + chart.margin.bottom - 14) + ")";
|
|
// X-axis label
|
|
chart.canvas.append("text") // text label for the x axis
|
|
.attr("transform", translation)
|
|
.attr("class", "x-axis-label")
|
|
.style("font-size", "14px")
|
|
.text(chart.labels["x-axis"])
|
|
.style("text-anchor", "middle");
|
|
// Y-axis label
|
|
chart.canvas.append("text")
|
|
.attr("transform", "rotate(-90)")
|
|
.attr("class", "y-axis-label")
|
|
.attr("y", 0 - chart.margin.left - yAxisBuffer)
|
|
.attr("x", 0 - (chart.height / 2))
|
|
.attr("dy", "1em")
|
|
.text(chart.labels["y-axis"])
|
|
.style("font-size", "14px")
|
|
.style("text-anchor", "middle");
|
|
// Title text
|
|
chart.canvas.append("text")
|
|
.attr("x", ((chart.width - yAxisBuffer) / 2))
|
|
.attr("y", 0 - (chart.margin.top / 2))
|
|
.attr("class", "title-text")
|
|
.text(chart.labels["title"])
|
|
.attr("text-anchor", "middle")
|
|
.style("font-size", "14px")
|
|
.style("text-decoration", "underline");
|
|
// Hidden watermark
|
|
chart.canvas.append("text")
|
|
.attr("x", 0)
|
|
.attr("y", 0)
|
|
.text("BlockPy")
|
|
.style("stroke", "#FDFDFD")
|
|
.style("font-size", "8px");
|
|
// Additional CSS
|
|
chart.svg.insert("defs", ":first-child")
|
|
.append("style")
|
|
.attr("type", "text/css")
|
|
.text("svg { background-color: white; }\n" +
|
|
".axis path,.axis line { fill: none; stroke: black; shape-rendering: crispEdges;}\n" +
|
|
".line { fill: none; stroke-width: 1px;}\n" +
|
|
".circle { r: 3; shape-rendering: crispEdges; }\n" +
|
|
".bar { shape-rendering: crispEdges;}\n");
|
|
return chart;
|
|
}
|
|
|
|
function getDigits(value) {
|
|
return value.toLocaleString().length;
|
|
}
|
|
|
|
function isEmpty(obj) {
|
|
return Object.keys(obj).length === 0;
|
|
}
|
|
|
|
function makeHistogram(plot) {
|
|
let domain = d3.extent(plot.data);
|
|
// Adjust bin sizes to capture last group properly
|
|
if (Array.isArray(plot.bins)) {
|
|
domain[1] += 1;
|
|
plot.bins[plot.bins.length-1] += 1;
|
|
}
|
|
let histogram = d3.histogram().domain(domain);
|
|
if (plot.bins) {
|
|
histogram = histogram.thresholds(plot.bins);
|
|
}
|
|
let result = histogram(plot.data);
|
|
// Unadjust bin sizes
|
|
if (Array.isArray(plot.bins)) {
|
|
result[result.length - 1].x1 -= 1;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function finalizeHistogram(chart) {
|
|
if (!chart.plots.some(plot => plot.type === "hist")) {
|
|
return false;
|
|
}
|
|
let yMax = 0;
|
|
for (let i = 0; i < chart.plots.length; i += 1) {
|
|
let plot = chart.plots[i];
|
|
if (plot.type !== "hist") {
|
|
continue;
|
|
}
|
|
let histogram = makeHistogram(plot);
|
|
yMax = Math.max(yMax, d3.max(histogram, (d) => d.length));
|
|
}
|
|
chart.extents.yMax = Math.max(yMax, chart.extents.yMax);
|
|
return true;
|
|
}
|
|
|
|
function setupXScale(chart, yAxisBuffer) {
|
|
// Calculate an appropriate offset based on the width of the numbers on the axis
|
|
// Set up x-scaling
|
|
chart.xScale = d3.scaleLinear()
|
|
.domain([chart.extents.xMin, chart.extents.xMax])
|
|
.range([0, chart.width - yAxisBuffer]);
|
|
// Set up x-axis
|
|
chart.xAxis = d3.axisBottom()
|
|
.scale(chart.xScale);
|
|
// Choose a smaller number of ticks if few discrete points
|
|
if (chart.ticks.xEstimate.size < 10) {
|
|
chart.xAxis.ticks(chart.ticks.xEstimate.size);
|
|
}
|
|
// If necessary, add in X ticks
|
|
if (!isEmpty(chart.ticks.x)) {
|
|
chart.xAxis = chart.xAxis.ticks().tickFormat(function (d) {
|
|
return chart.ticks.x[d];
|
|
});
|
|
}
|
|
}
|
|
|
|
function setupYScale(chart) {
|
|
chart.yScale = d3.scaleLinear()
|
|
.domain([chart.extents.yMin, chart.extents.yMax])
|
|
.range([chart.height, 0]);
|
|
chart.yAxis = d3.axisLeft()
|
|
.scale(chart.yScale);
|
|
// Choose a smaller number of ticks if few discrete points
|
|
if (chart.ticks.yEstimate.size < 10) {
|
|
chart.yAxis.ticks(chart.ticks.yEstimate.size);
|
|
}
|
|
// If necessary, add in Y ticks
|
|
if (!isEmpty(chart.ticks.y)) {
|
|
chart.yAxis = chart.yAxis.ticks().tickFormat(function (d) {
|
|
return chart.ticks.y[d];
|
|
});
|
|
}
|
|
}
|
|
|
|
function finalizeChart(chart) {
|
|
let doctype = "<?xml version=\"1.0\" standalone=\"no\"?>" + "<" + "!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">";
|
|
let xml = new XMLSerializer().serializeToString(chart.svg.node());
|
|
let blob = new Blob([doctype + xml], {type: "image/svg+xml"});
|
|
let url = window.URL.createObjectURL(blob);
|
|
//var data = "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(xml)));
|
|
let anchor = document.createElement("a");
|
|
let img = document.createElement("img");
|
|
img.style.display = "block";
|
|
let oldChart = chart;
|
|
let filename = chart.labels.title;
|
|
resetChart();
|
|
//outputTarget.html[0].appendChild(img);
|
|
// TODO: Consider using the SVG as the actual image, and using this as the href
|
|
// surrounding it instead.
|
|
oldChart.svg.node().parentNode.replaceChild(anchor, oldChart.svg.node());
|
|
img.onload = function () {
|
|
img.onload = null;
|
|
//TODO: Make this capture a class descendant. Cross the D3/Jquery barrier!
|
|
let canvas = document.createElement("canvas");
|
|
canvas.width = getWidth();
|
|
canvas.height = getHeight();
|
|
let ctx = canvas.getContext("2d");
|
|
ctx.drawImage(img, 0, 0);
|
|
let canvasUrl = canvas.toDataURL("image/png");
|
|
img.setAttribute("src", canvasUrl);
|
|
img.setAttribute("title", "Generated plot titled: " + filename);
|
|
img.setAttribute("alt", "Generated plot titled: " + filename);
|
|
anchor.setAttribute("href", canvasUrl);
|
|
anchor.setAttribute("download", filename);
|
|
// Snip off this chart, we can now start a new one.
|
|
};
|
|
img.onerror = function (e) {
|
|
console.error(e);
|
|
};
|
|
img.setAttribute("src", url);
|
|
anchor.appendChild(img);
|
|
}
|
|
|
|
const DEFAULT_MARKER_SIZE = 2;
|
|
|
|
let show_f = function () {
|
|
let chart = getChart();
|
|
|
|
// Sanity checks
|
|
if (chart.plots.length === 0) {
|
|
return;
|
|
}
|
|
if (chart.extents.xMin === undefined || chart.extents.yMin === undefined) {
|
|
return;
|
|
}
|
|
|
|
// Establish x/y scalers and axes
|
|
let yAxisBuffer = 5 * Math.max(getDigits(chart.extents.xMin), getDigits(chart.extents.xMax),
|
|
getDigits(chart.extents.yMin), getDigits(chart.extents.yMin));
|
|
setupXScale(chart, yAxisBuffer);
|
|
finalizeHistogram(chart);
|
|
setupYScale(chart);
|
|
|
|
chart.mapX = d => chart.xScale(d.x);
|
|
chart.mapY = d => chart.yScale(d.y);
|
|
chart.mapLine = d3.line().x(chart.mapX).y(chart.mapY);
|
|
attachCanvasToChart(chart, yAxisBuffer);
|
|
|
|
// Actually draw the chart objects
|
|
for (let i = 0; i < chart.plots.length; i += 1) {
|
|
let plot = chart.plots[i];
|
|
switch (plot.type) {
|
|
case "line":
|
|
//设置线形
|
|
var line_style=plot.style["linestyle"];
|
|
if(line_style=='--'){
|
|
chart.canvas.append("path")
|
|
.style("fill", "none")
|
|
.style("stroke", plot.style.color)
|
|
.style("stroke-width", plot.style["linewidth"])
|
|
.style("fill-opacity", plot.style["alpha"])
|
|
.style("stroke-dasharray","10,8")
|
|
.attr("class", "line")
|
|
//.data(plot.data)
|
|
.attr("d", chart.mapLine(plot.data));
|
|
}else if(line_style=='-.'){
|
|
chart.canvas.append("path")
|
|
.style("fill", "none")
|
|
.style("stroke", plot.style.color)
|
|
.style("stroke-width", plot.style["linewidth"])
|
|
.style("fill-opacity", plot.style["alpha"])
|
|
.style("stroke-dasharray","10,5,2,5")
|
|
.attr("class", "line")
|
|
//.data(plot.data)
|
|
.attr("d", chart.mapLine(plot.data));
|
|
}else if(line_style==':'){
|
|
chart.canvas.append("path")
|
|
.style("fill", "none")
|
|
.style("stroke", plot.style.color)
|
|
.style("stroke-width", plot.style["linewidth"])
|
|
.style("fill-opacity", plot.style["alpha"])
|
|
.style("stroke-dasharray","2,3")
|
|
.attr("class", "line")
|
|
//.data(plot.data)
|
|
.attr("d", chart.mapLine(plot.data));
|
|
}else{
|
|
chart.canvas.append("path")
|
|
.style("fill", "none")
|
|
.style("stroke", plot.style.color)
|
|
.style("stroke-width", plot.style["linewidth"])
|
|
.style("fill-opacity", plot.style["alpha"])
|
|
.attr("class", "line")
|
|
//.data(plot.data)
|
|
.attr("d", chart.mapLine(plot.data));
|
|
}
|
|
|
|
//再画个点上去
|
|
var dot_marker=plot.style["marker"];
|
|
if(dot_marker==','){//像素点
|
|
break;
|
|
}else if(dot_marker=='o'){//大圆点
|
|
chart.canvas.append("g")
|
|
.attr("class", "series")
|
|
.selectAll(".point")
|
|
.data(plot.data)
|
|
// Entering
|
|
.enter()
|
|
.append("circle")
|
|
.style("fill", (d, i) => {
|
|
if (plot.colors == null) {
|
|
return plot.style.color;
|
|
} else if (Array.isArray(plot.colors)) {
|
|
return plot.colors[i] || plot.style.color;
|
|
} else {
|
|
return plot.colors;
|
|
}
|
|
})
|
|
.attr("class", "dot_of_line")
|
|
.attr("cx", chart.mapX)
|
|
.attr("cy", chart.mapY)
|
|
.attr("r", 4);
|
|
}else{
|
|
chart.canvas.append("g")
|
|
.attr("class", "series")
|
|
.selectAll(".point")
|
|
.data(plot.data)
|
|
// Entering
|
|
.enter()
|
|
.append("circle")
|
|
.style("fill", (d, i) => {
|
|
if (plot.colors == null) {
|
|
return plot.style.color;
|
|
} else if (Array.isArray(plot.colors)) {
|
|
return plot.colors[i] || plot.style.color;
|
|
} else {
|
|
return plot.colors;
|
|
}
|
|
})
|
|
.attr("class", "dot_of_line")
|
|
.attr("cx", chart.mapX)
|
|
.attr("cy", chart.mapY)
|
|
.attr("r", 2);
|
|
}
|
|
|
|
break;
|
|
case "one_line":
|
|
chart.canvas.append("line")
|
|
.style("stroke", plot.style.color)
|
|
.style("stroke-width", plot.style["linewidth"])
|
|
.style("fill-opacity", plot.style["alpha"])
|
|
.attr("x1", chart.xScale(plot.data.x1))
|
|
.attr("x2", chart.xScale(plot.data.x2))
|
|
.attr("y1", chart.yScale(plot.data.y1) )
|
|
.attr("y2", chart.yScale(plot.data.y2) );
|
|
break;
|
|
case "scatter":
|
|
var dot_marker=plot.style["marker"];
|
|
if(dot_marker==','){//像素点
|
|
chart.canvas.append("g")
|
|
.attr("class", "series")
|
|
.selectAll(".point")
|
|
.data(plot.data)
|
|
// Entering
|
|
.enter()
|
|
.append('rect')
|
|
.style("fill", (d, i) => {
|
|
if (plot.colors == null) {
|
|
return plot.style.color;
|
|
} else if (Array.isArray(plot.colors)) {
|
|
return plot.colors[i] || plot.style.color;
|
|
} else {
|
|
return plot.colors;
|
|
}
|
|
})
|
|
.attr("class", "rect")
|
|
.attr("x", chart.mapX)
|
|
.attr("y", chart.mapY)
|
|
.attr("width",5)
|
|
.attr("height",5)
|
|
break;
|
|
}else if(dot_marker=='o'){//大圆点
|
|
chart.canvas.append("g")
|
|
.attr("class", "series")
|
|
.selectAll(".point")
|
|
.data(plot.data)
|
|
// Entering
|
|
.enter()
|
|
.append("circle")
|
|
.style("fill", (d, i) => {
|
|
if (plot.colors == null) {
|
|
return plot.style.color;
|
|
} else if (Array.isArray(plot.colors)) {
|
|
return plot.colors[i] || plot.style.color;
|
|
} else {
|
|
return plot.colors;
|
|
}
|
|
})
|
|
.attr("class", "scatter_dot")
|
|
.attr("cx", chart.mapX)
|
|
.attr("cy", chart.mapY)
|
|
.attr("r", 5);
|
|
}else{
|
|
chart.canvas.append("g")
|
|
.attr("class", "series")
|
|
.selectAll(".point")
|
|
.data(plot.data)
|
|
// Entering
|
|
.enter()
|
|
.append("circle")
|
|
.style("fill", (d, i) => {
|
|
if (plot.colors == null) {
|
|
return plot.style.color;
|
|
} else if (Array.isArray(plot.colors)) {
|
|
return plot.colors[i] || plot.style.color;
|
|
} else {
|
|
return plot.colors;
|
|
}
|
|
})
|
|
.attr("class", "scatter_dot")
|
|
.attr("cx", chart.mapX)
|
|
.attr("cy", chart.mapY)
|
|
.attr("r", 2.5);
|
|
}
|
|
break;
|
|
case "hist":
|
|
let histogram = makeHistogram(plot);
|
|
chart.canvas.selectAll(".bar")
|
|
.data(histogram)
|
|
// Enter
|
|
.enter()
|
|
.append("rect")
|
|
.attr("class", "bar")
|
|
.style("fill", plot["style"]["color"])
|
|
.style("stroke", "black")
|
|
.attr("x", function (d) {
|
|
// TODO: Handle align
|
|
return chart.xScale(d.x0);
|
|
})
|
|
.attr("width", (d) => chart.xScale(d.x1) - chart.xScale(d.x0))
|
|
.attr("y", function (d) {
|
|
return chart.yScale(d.length);
|
|
})
|
|
.attr("height", function (d) {
|
|
return chart.height - chart.yScale(d.length);
|
|
});
|
|
break;
|
|
case "bar":
|
|
chart.canvas.selectAll(".bar")
|
|
.data(plot.data)
|
|
// Enter
|
|
.enter()
|
|
.append("rect")
|
|
.attr("class", "bar")
|
|
.style("fill", plot["style"]["color"])
|
|
.style("stroke", "black")
|
|
.attr("x", function (d) {
|
|
let increment = (plot.align === "center" ? plot.width/2 : 0);
|
|
return chart.xScale(d.x-increment);
|
|
})
|
|
.attr("width", function (d) {
|
|
return chart.xScale(d.x+plot.width) - chart.xScale(d.x);
|
|
})
|
|
.attr("y", chart.mapY)
|
|
.attr("height", function (d) {
|
|
return chart.height - chart.yScale(d.y);
|
|
});
|
|
break;
|
|
case "boxplot":
|
|
const WIDTH = .6;
|
|
chart.canvas.append("line")
|
|
.attr("x1", chart.xScale(plot.data.x))
|
|
.attr("x2", chart.xScale(plot.data.x))
|
|
.attr("y1", chart.yScale(plot.data.y[0]) )
|
|
.attr("y2", chart.yScale(plot.data.y[1]) )
|
|
.attr("stroke", "black");
|
|
chart.canvas.append("line")
|
|
.attr("x1", chart.xScale(plot.data.x))
|
|
.attr("x2", chart.xScale(plot.data.x))
|
|
.attr("y1", chart.yScale(plot.data.y[3]) )
|
|
.attr("y2", chart.yScale(plot.data.y[4]) )
|
|
.attr("stroke", "black");
|
|
chart.canvas.append("rect")
|
|
.attr("x", chart.xScale(plot.data.x-WIDTH/2))
|
|
.attr("y", chart.yScale(plot.data.y[3]) )
|
|
.attr("height", (chart.yScale(plot.data.y[1])-chart.yScale(plot.data.y[3])))
|
|
.attr("width", chart.xScale(WIDTH) )
|
|
.attr("stroke", "black")
|
|
.style("fill", "none");
|
|
chart.canvas.selectAll(".boxplot")
|
|
.data([plot.data.y[0], plot.data.y[2], plot.data.y[4]])
|
|
.enter()
|
|
.append("line")
|
|
.attr("x1", chart.xScale(plot.data.x-WIDTH/2))
|
|
.attr("x2", chart.xScale(plot.data.x+WIDTH/2))
|
|
.attr("y1", chart.yScale )
|
|
.attr("y2", chart.yScale )
|
|
.attr("class", "boxplot")
|
|
.attr("stroke", "black");
|
|
break;
|
|
}
|
|
}
|
|
////////////////////////////////////////////////////////////////////
|
|
|
|
finalizeChart(chart);
|
|
};
|
|
mod.show = new Sk.builtin.func(show_f);
|
|
|
|
let title_f = function (s) {
|
|
Sk.builtin.pyCheckArgs("title", arguments, 1, 1);
|
|
|
|
if (!Sk.builtin.checkString(s)) {
|
|
throw new Sk.builtin.TypeError("'" + Sk.abstr.typeName(s) +
|
|
"' is not supported for title; should be a string.");
|
|
}
|
|
|
|
getChart().labels["title"] = Sk.ffi.remapToJs(s);
|
|
};
|
|
mod.title = new Sk.builtin.func(title_f);
|
|
|
|
let xlabel_f = function (s) {
|
|
Sk.builtin.pyCheckArgs("xlabel", arguments, 1, 1);
|
|
|
|
if (!Sk.builtin.checkString(s)) {
|
|
throw new Sk.builtin.TypeError("'" + Sk.abstr.typeName(s) +
|
|
"' is not supported for xlabel; should be a string.");
|
|
}
|
|
getChart().labels["x-axis"] = Sk.ffi.remapToJs(s);
|
|
};
|
|
mod.xlabel = new Sk.builtin.func(xlabel_f);
|
|
|
|
let ylabel_f = function (s) {
|
|
Sk.builtin.pyCheckArgs("ylabel", arguments, 1, 1);
|
|
|
|
if (!Sk.builtin.checkString(s)) {
|
|
throw new Sk.builtin.TypeError("'" + Sk.abstr.typeName(s) +
|
|
"' is not supported for ylabel; should be a string.");
|
|
}
|
|
getChart().labels["y-axis"] = Sk.ffi.remapToJs(s);
|
|
};
|
|
mod.ylabel = new Sk.builtin.func(ylabel_f);
|
|
|
|
let xticks_f = function (kwa) {
|
|
Sk.builtin.pyCheckArgs("xticks", arguments, 0, Infinity, true, false);
|
|
let args = Array.prototype.slice.call(arguments, 1);
|
|
let kwargs = new Sk.builtins.dict(kwa); // Get argument as object
|
|
|
|
let rotation = getKeywordParameter(kwargs, STRING_ROTATION, "horizontal");
|
|
|
|
let chart = getChart();
|
|
if (args.length === 1) {
|
|
chart.ticks.x = {};
|
|
for (let i=0; i<args[0].length;i+= 1) {
|
|
chart.ticks.x[i] = i;
|
|
}
|
|
} else if (args.length === 2) {
|
|
chart.ticks.x = {};
|
|
for (let i=0; i<args[0].length;i+= 1) {
|
|
chart.ticks.x[i] = args[2][i];
|
|
}
|
|
}
|
|
chart.ticks.xRotate = rotation;
|
|
};
|
|
xticks_f.co_kwargs = true;
|
|
mod.xticks = new Sk.builtin.func(xticks_f);
|
|
|
|
// Clear the current figure
|
|
let clf_f = function () {
|
|
resetChart();
|
|
};
|
|
mod.clf = new Sk.builtin.func(clf_f);
|
|
|
|
const UNSUPPORTED = ["semilogx", "semilogy", "specgram", "stackplot", "stem", "step", "streamplot",
|
|
"tricontour", "tricontourf", "tripcolor", "triplot", "xcorr", "barbs",
|
|
"cla", "grid", "table", "text", "annotate", "ticklabel_format", "locator_params",
|
|
"tick_params", "margins", "autoscale", "autumn", "cool", "copper", "flag", "gray",
|
|
"hot", "hsv", "jet", "pink", "prism", "spring", "summer", "winter", "spectral",
|
|
"loglog", "magnitude_spectrum", "pcolor", "pcolormesh", "phase_spectrum",
|
|
"pie", "plot_date", "psd", "quiver", "quiverkey", "findobj", "switch_backend",
|
|
"isinteractive", "ioff", "ion", "pause", "rc", "rc_context", "rcdefaults",
|
|
"gci", "sci", "xkcd", "figure", "gcf", "get_fignums", "get_figlabels",
|
|
"get_current_fig_manager", "connect", "disconnect", "close", "savefig",
|
|
"ginput", "waitforbuttonpress", "figtext", "suptitle", "figimage", "figlegend",
|
|
"hold", "ishold", "over", "delaxes", "sca", "gca", "subplot", "subplots",
|
|
"subplot2grid", "twinx", "twiny", "subplots_adjust", "subplot_tool",
|
|
"tight_layout", "box", "xlim", "ylim", "xscale", "yscale", "yticks",
|
|
"minorticks_on", "minorticks_off", "rgrids", "thetagrids", "plotting", "get_plot_commands",
|
|
"colors", "colormaps", "_setup_pyplot_info_docstrings", "colorbar", "clim", "set_cmap",
|
|
"imread", "imsave", "matshow", "polar", "plotfile", "_autogen_docstring", "acorr",
|
|
"arrow", "axhline", "axhspan", "axvline", "axvspan",, "broken_barh", "cohere",
|
|
"clabel", "contour", "contourf", "csd", "errorbar", "eventplot", "fill", "fill_between",
|
|
"fill_betweenx", "hexbin", "hist2d", "axis"];
|
|
|
|
for (let i = 0; i < UNSUPPORTED.length; i += 1) {
|
|
mod[UNSUPPORTED[i]] = new Sk.builtin.func(function () {
|
|
throw new Sk.builtin.NotImplementedError(UNSUPPORTED[i] + " is not yet implemented");
|
|
});
|
|
}
|
|
|
|
let legend_f = function () {
|
|
return Sk.builtin.none.none$;
|
|
};
|
|
mod.legend = new Sk.builtin.func(legend_f);
|
|
|
|
|
|
return mod;
|
|
};
|
|
|
|
//TODO: Make font size controllable
|
|
Sk.jsplotlib.rc = {
|
|
"lines.linewidth": 1.0,
|
|
"lines.linestyle": "-",
|
|
"lines.color": "blue",
|
|
"lines.marker": "None",
|
|
"lines.markeredgewidth": 0.5,
|
|
"lines.markersize": 6,
|
|
"lines.dash_joinstyle": "miter",
|
|
"lines.dash_capstyle": "butt",
|
|
"lines.solid_jointyle": "miter",
|
|
"lines.solid_capstyle": "projecting",
|
|
"lines.antialiased": true,
|
|
"patch.linewidth": 1.0,
|
|
"patch.facecolor": "blue",
|
|
"patch.edgecolor": "black",
|
|
"patch.antialiased": true,
|
|
"text.color": "black",
|
|
"axes.hold": true, // whether to clear the axes by default on
|
|
"axes.facecolor": "white", // axes background color
|
|
"axes.edgecolor": "black", // axes edge color
|
|
"axes.grid": false,
|
|
"axes.titlesize": "large",
|
|
"axes.labelsize": "medium",
|
|
"axes.labelweigth": "normal",
|
|
"axes.labelcolor": "black",
|
|
"axes.axisbelow": false,
|
|
"axes.color_cycle": ["b", "g", "r", "c", "m", "y", "k"]
|
|
};
|
|
|
|
Sk.jsplotlib._line_counter = 0;
|
|
|
|
/** List of all supported line styles **/
|
|
Sk.jsplotlib.lineStyles = {
|
|
"-": "_draw_solid",
|
|
"--": "_draw_dashed",
|
|
"-.": "_draw_dash_dot",
|
|
":": "_draw_dotted",
|
|
"None": "_draw_nothing",
|
|
" ": "_draw_nothing",
|
|
"": "_draw_nothing",
|
|
};
|
|
|
|
Sk.jsplotlib.lineMarkers = {
|
|
".": "point",
|
|
",": "pixel",
|
|
"o": "circle",
|
|
"v": "triangle_down",
|
|
"^": "triangle_up",
|
|
"<": "triangle_left",
|
|
">": "triangle_right",
|
|
"1": "tri_down",
|
|
"2": "tri_up",
|
|
"3": "tri_left",
|
|
"4": "tri_right",
|
|
"8": "octagon",
|
|
"s": "square",
|
|
"p": "pentagon",
|
|
"*": "star",
|
|
"h": "hexagon1",
|
|
"H": "hexagon2",
|
|
"+": "plus",
|
|
"x": "x",
|
|
"D": "diamond",
|
|
"d": "thin_diamond",
|
|
"|": "vline",
|
|
"_": "hline",
|
|
//TICKLEFT: 'tickleft',
|
|
//TICKRIGHT: 'tickright',
|
|
//TICKUP: 'tickup',
|
|
//TICKDOWN: 'tickdown',
|
|
//CARETLEFT: 'caretleft',
|
|
//CARETRIGHT: 'caretright',
|
|
//CARETUP: 'caretup',
|
|
//CARETDOWN: 'caretdown',
|
|
"None": "nothing",
|
|
//Sk.builtin.none.none$: 'nothing',
|
|
" ": "nothing",
|
|
"": "nothing"
|
|
};
|
|
|
|
/**
|
|
Color short keys
|
|
**/
|
|
Sk.jsplotlib.colors = {
|
|
"b": "blue",
|
|
"g": "green",
|
|
"r": "red",
|
|
"c": "cyan",
|
|
"m": "magenta",
|
|
"y": "yellow",
|
|
"k": "black",
|
|
"w": "white"
|
|
};
|
|
|
|
/**
|
|
Mapping of all possible CSS colors, that are supported by matplotlib
|
|
**/
|
|
Sk.jsplotlib.cnames = {
|
|
"aliceblue": "#F0F8FF",
|
|
"antiquewhite": "#FAEBD7",
|
|
"aqua": "#00FFFF",
|
|
"aquamarine": "#7FFFD4",
|
|
"azure": "#F0FFFF",
|
|
"beige": "#F5F5DC",
|
|
"bisque": "#FFE4C4",
|
|
"black": "#000000",
|
|
"blanchedalmond": "#FFEBCD",
|
|
"blue": "#0000FF",
|
|
"blueviolet": "#8A2BE2",
|
|
"brown": "#A52A2A",
|
|
"burlywood": "#DEB887",
|
|
"cadetblue": "#5F9EA0",
|
|
"chartreuse": "#7FFF00",
|
|
"chocolate": "#D2691E",
|
|
"coral": "#FF7F50",
|
|
"cornflowerblue": "#6495ED",
|
|
"cornsilk": "#FFF8DC",
|
|
"crimson": "#DC143C",
|
|
"cyan": "#00FFFF",
|
|
"darkblue": "#00008B",
|
|
"darkcyan": "#008B8B",
|
|
"darkgoldenrod": "#B8860B",
|
|
"darkgray": "#A9A9A9",
|
|
"darkgreen": "#006400",
|
|
"darkkhaki": "#BDB76B",
|
|
"darkmagenta": "#8B008B",
|
|
"darkolivegreen": "#556B2F",
|
|
"darkorange": "#FF8C00",
|
|
"darkorchid": "#9932CC",
|
|
"darkred": "#8B0000",
|
|
"darksage": "#598556",
|
|
"darksalmon": "#E9967A",
|
|
"darkseagreen": "#8FBC8F",
|
|
"darkslateblue": "#483D8B",
|
|
"darkslategray": "#2F4F4F",
|
|
"darkturquoise": "#00CED1",
|
|
"darkviolet": "#9400D3",
|
|
"deeppink": "#FF1493",
|
|
"deepskyblue": "#00BFFF",
|
|
"dimgray": "#696969",
|
|
"dodgerblue": "#1E90FF",
|
|
"firebrick": "#B22222",
|
|
"floralwhite": "#FFFAF0",
|
|
"forestgreen": "#228B22",
|
|
"fuchsia": "#FF00FF",
|
|
"gainsboro": "#DCDCDC",
|
|
"ghostwhite": "#F8F8FF",
|
|
"gold": "#FFD700",
|
|
"goldenrod": "#DAA520",
|
|
"gray": "#808080",
|
|
"green": "#008000",
|
|
"greenyellow": "#ADFF2F",
|
|
"honeydew": "#F0FFF0",
|
|
"hotpink": "#FF69B4",
|
|
"indianred": "#CD5C5C",
|
|
"indigo": "#4B0082",
|
|
"ivory": "#FFFFF0",
|
|
"khaki": "#F0E68C",
|
|
"lavender": "#E6E6FA",
|
|
"lavenderblush": "#FFF0F5",
|
|
"lawngreen": "#7CFC00",
|
|
"lemonchiffon": "#FFFACD",
|
|
"lightblue": "#ADD8E6",
|
|
"lightcoral": "#F08080",
|
|
"lightcyan": "#E0FFFF",
|
|
"lightgoldenrodyellow": "#FAFAD2",
|
|
"lightgreen": "#90EE90",
|
|
"lightgray": "#D3D3D3",
|
|
"lightpink": "#FFB6C1",
|
|
"lightsage": "#BCECAC",
|
|
"lightsalmon": "#FFA07A",
|
|
"lightseagreen": "#20B2AA",
|
|
"lightskyblue": "#87CEFA",
|
|
"lightslategray": "#778899",
|
|
"lightsteelblue": "#B0C4DE",
|
|
"lightyellow": "#FFFFE0",
|
|
"lime": "#00FF00",
|
|
"limegreen": "#32CD32",
|
|
"linen": "#FAF0E6",
|
|
"magenta": "#FF00FF",
|
|
"maroon": "#800000",
|
|
"mediumaquamarine": "#66CDAA",
|
|
"mediumblue": "#0000CD",
|
|
"mediumorchid": "#BA55D3",
|
|
"mediumpurple": "#9370DB",
|
|
"mediumseagreen": "#3CB371",
|
|
"mediumslateblue": "#7B68EE",
|
|
"mediumspringgreen": "#00FA9A",
|
|
"mediumturquoise": "#48D1CC",
|
|
"mediumvioletred": "#C71585",
|
|
"midnightblue": "#191970",
|
|
"mintcream": "#F5FFFA",
|
|
"mistyrose": "#FFE4E1",
|
|
"moccasin": "#FFE4B5",
|
|
"navajowhite": "#FFDEAD",
|
|
"navy": "#000080",
|
|
"oldlace": "#FDF5E6",
|
|
"olive": "#808000",
|
|
"olivedrab": "#6B8E23",
|
|
"orange": "#FFA500",
|
|
"orangered": "#FF4500",
|
|
"orchid": "#DA70D6",
|
|
"palegoldenrod": "#EEE8AA",
|
|
"palegreen": "#98FB98",
|
|
"paleturquoise": "#AFEEEE",
|
|
"palevioletred": "#DB7093",
|
|
"papayawhip": "#FFEFD5",
|
|
"peachpuff": "#FFDAB9",
|
|
"peru": "#CD853F",
|
|
"pink": "#FFC0CB",
|
|
"plum": "#DDA0DD",
|
|
"powderblue": "#B0E0E6",
|
|
"purple": "#800080",
|
|
"red": "#FF0000",
|
|
"rosybrown": "#BC8F8F",
|
|
"royalblue": "#4169E1",
|
|
"saddlebrown": "#8B4513",
|
|
"salmon": "#FA8072",
|
|
"sage": "#87AE73",
|
|
"sandybrown": "#FAA460",
|
|
"seagreen": "#2E8B57",
|
|
"seashell": "#FFF5EE",
|
|
"sienna": "#A0522D",
|
|
"silver": "#C0C0C0",
|
|
"skyblue": "#87CEEB",
|
|
"slateblue": "#6A5ACD",
|
|
"slategray": "#708090",
|
|
"snow": "#FFFAFA",
|
|
"springgreen": "#00FF7F",
|
|
"steelblue": "#4682B4",
|
|
"tan": "#D2B48C",
|
|
"teal": "#008080",
|
|
"thistle": "#D8BFD8",
|
|
"tomato": "#FF6347",
|
|
"turquoise": "#40E0D0",
|
|
"violet": "#EE82EE",
|
|
"wheat": "#F5DEB3",
|
|
"white": "#FFFFFF",
|
|
"whitesmoke": "#F5F5F5",
|
|
"yellow": "#FFFF00",
|
|
"yellowgreen": "#9ACD32"
|
|
};
|
|
|
|
Sk.jsplotlib.color_to_hex = function (color) {
|
|
// is color a shortcut?
|
|
if (Sk.jsplotlib.colors[color]) {
|
|
color = Sk.jsplotlib.colors[color];
|
|
}
|
|
|
|
// is inside cnames array?
|
|
if (Sk.jsplotlib.cnames[color]) {
|
|
return Sk.jsplotlib.cnames[color];
|
|
}
|
|
|
|
// check if it is already a hex value
|
|
if (typeof color == "string") {
|
|
var match = color.match(/^#(?:[0-9a-fA-F]{3}){1,2}$/);
|
|
if (match && match.length === 1) {
|
|
return match[0];
|
|
}
|
|
}
|
|
|
|
// add rgb colors here
|
|
if (Array.isArray(color) && color.length === 3) {
|
|
return Sk.jsplotlib.rgb2hex(color);
|
|
}
|
|
|
|
// back to default
|
|
return Sk.jsplotlib.cnames[Sk.jsplotlib.rc["lines.color"]];
|
|
};
|
|
|
|
Sk.jsplotlib.get_color = function (cs) {
|
|
return Sk.jsplotlib.colors[cs] ? Sk.jsplotlib.colors[cs] : Sk.jsplotlib.colors.b;
|
|
};
|
|
|
|
Sk.jsplotlib.parse_marker = function (style) {
|
|
if (!style) {
|
|
return "x";
|
|
}
|
|
switch (style) {
|
|
case ".":
|
|
return ".";
|
|
case ",":
|
|
return ",";
|
|
case "o":
|
|
return "o";
|
|
case "v":
|
|
return "x";
|
|
case "^":
|
|
return "x";
|
|
case "<":
|
|
return "x";
|
|
case ">":
|
|
return "x";
|
|
case "1":
|
|
return "x";
|
|
case "2":
|
|
return "x";
|
|
case "3":
|
|
return "x";
|
|
case "4":
|
|
return "x";
|
|
case "s":
|
|
return "x";
|
|
case "p":
|
|
return "x";
|
|
case "*":
|
|
return "x";
|
|
case "h":
|
|
return "x";
|
|
case "H":
|
|
return "x";
|
|
case "+":
|
|
return "x";
|
|
case "x":
|
|
return "x";
|
|
case "D":
|
|
return "x";
|
|
case "d":
|
|
return "x";
|
|
case "|":
|
|
return "x";
|
|
case "_":
|
|
return "x";
|
|
default:
|
|
return "";
|
|
}
|
|
};
|
|
|
|
/**
|
|
Process a MATLAB style color/line style format string. Return a
|
|
(*linestyle*, *color*) tuple as a result of the processing. Default
|
|
values are ('-', 'b'). Example format strings include:
|
|
* 'ko': black circles
|
|
* '.b': blue dots
|
|
* 'r--': red dashed lines
|
|
.. seealso::
|
|
:func:`~matplotlib.Line2D.lineStyles` and
|
|
:func:`~matplotlib.pyplot.colors`
|
|
for all possible styles and color format string.
|
|
**/
|
|
Sk.jsplotlib._process_plot_format = function (fmt) {
|
|
var linestyle = null;
|
|
var marker = null;
|
|
var color = null;
|
|
|
|
// Is fmt just a colorspec
|
|
try {
|
|
color = Sk.jsplotlib.to_rgb(fmt);
|
|
if (color) {
|
|
return {
|
|
"linestyle": linestyle,
|
|
"marker": marker,
|
|
"color": color
|
|
};
|
|
}
|
|
} catch (e) {
|
|
}
|
|
|
|
// handle the multi char special cases and strip them for the string
|
|
if (fmt.search(/--/) >= 0) {
|
|
linestyle = "--";
|
|
fmt = fmt.replace(/--/, "");
|
|
}
|
|
if (fmt.search(/-\./) >= 0) {
|
|
linestyle = "-.";
|
|
fmt = fmt.replace(/-\./, "");
|
|
}
|
|
if (fmt.search(/ /) >= 0) {
|
|
linestyle = "";
|
|
fmt = fmt.replace(/ /, "");
|
|
}
|
|
|
|
var i;
|
|
for (i = 0; i < fmt.length; i++) {
|
|
var c = fmt.charAt(i);
|
|
if (Sk.jsplotlib.lineStyles[c]) {
|
|
if (linestyle) {
|
|
throw new Sk.builtin.ValueError("Illegal format string \"" + fmt +
|
|
"\"; two linestyle symbols");
|
|
}
|
|
linestyle = c;
|
|
} else if (Sk.jsplotlib.lineMarkers[c]) {
|
|
if (marker) {
|
|
throw new Sk.builtin.ValueError("Illegal format string \"" + fmt +
|
|
"\"; two marker symbols");
|
|
}
|
|
marker = c;
|
|
} else if (Sk.jsplotlib.colors[c]) {
|
|
if (color) {
|
|
throw new Sk.builtin.ValueError("Illegal format string \"" + fmt +
|
|
"\"; two color symbols");
|
|
}
|
|
color = c;
|
|
} else {
|
|
throw new Sk.builtin.ValueError("Unrecognized character " + c +
|
|
" in format string");
|
|
}
|
|
}
|
|
|
|
if (!linestyle && !marker) {
|
|
// use defaults --> rcParams['lines.linestyle']
|
|
linestyle = "-";
|
|
}
|
|
if (!linestyle) {
|
|
linestyle = " ";
|
|
}
|
|
if (!marker) {
|
|
marker = "";
|
|
}
|
|
|
|
return {
|
|
"linestyle": linestyle,
|
|
"marker": marker,
|
|
"color": color
|
|
};
|
|
};
|
|
|
|
/**
|
|
https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/colors.py
|
|
http://matplotlib.org/api/colors_api.html
|
|
Returns an *RGB* tuple of three floats from 0-1.
|
|
*arg* can be an *RGB* or *RGBA* sequence or a string in any of
|
|
several forms:
|
|
1) a letter from the set 'rgbcmykw'
|
|
2) a hex color string, like '#00FFFF'
|
|
3) a standard name, like 'aqua'
|
|
4) a string representation of a float, like '0.4',
|
|
indicating gray on a 0-1 scale
|
|
if *arg* is *RGBA*, the *A* will simply be discarded.
|
|
**/
|
|
Sk.jsplotlib.to_rgb = function (fmt) {
|
|
if (!fmt) {
|
|
return null;
|
|
}
|
|
|
|
var color = null;
|
|
|
|
if (typeof fmt == "string") {
|
|
let fmt_lower = fmt.toLowerCase();
|
|
|
|
if (Sk.jsplotlib.colors[fmt_lower]) {
|
|
return Sk.jsplotlib.hex2color(Sk.jsplotlib.cnames[Sk.jsplotlib.colors[fmt_lower]]);
|
|
}
|
|
|
|
// is inside cnames array?
|
|
if (Sk.jsplotlib.cnames[fmt_lower]) {
|
|
return Sk.jsplotlib.hex2color(Sk.jsplotlib.cnames[fmt_lower]);
|
|
}
|
|
|
|
if (fmt_lower.indexOf("#") === 0) {
|
|
return Sk.jsplotlib.hex2color(fmt_lower);
|
|
}
|
|
|
|
// is it simple grey shade?
|
|
var fl = parseFloat(fmt_lower);
|
|
if (isNaN(fl)) {
|
|
throw new Sk.builtin.ValueError("cannot convert argument to rgb sequence");
|
|
}
|
|
|
|
if (fl < 0 || fl > 1) {
|
|
throw new Sk.builtin.ValueError("gray (string) must be in range 0-1");
|
|
}
|
|
|
|
return [fl, fl, fl];
|
|
}
|
|
|
|
// check if its a color tuple [r,g,b, [a]] with values from [0-1]
|
|
if (Array.isArray(fmt)) {
|
|
if (fmt.length > 4 || fmt.length < 3) {
|
|
throw new Sk.builtin.ValueError("sequence length is " + fmt.length +
|
|
"; must be 3 or 4");
|
|
}
|
|
|
|
color = fmt.slice(0, 3);
|
|
var i;
|
|
|
|
for (i = 0; i < 3; i++) {
|
|
var fl_rgb = parseFloat(fmt);
|
|
|
|
if (fl_rgb < 0 || fl_rgb > 1) {
|
|
throw new Sk.builtin.ValueError(
|
|
"number in rbg sequence outside 0-1 range");
|
|
}
|
|
}
|
|
}
|
|
|
|
return color;
|
|
};
|
|
|
|
/**
|
|
Take a hex string *s* and return the corresponding rgb 3-tuple
|
|
Example: #efefef -> (0.93725, 0.93725, 0.93725)
|
|
**/
|
|
Sk.jsplotlib.hex2color = function (s) {
|
|
if (!s || typeof s != "string") {
|
|
throw new Sk.builtin.TypeError("hex2color requires a string argument");
|
|
}
|
|
// check if it is a hex value
|
|
var i;
|
|
var s_copy = s;
|
|
var hex_tuple = [];
|
|
for (i = 0; i < 3; i++) {
|
|
var match = s_copy.match(/(?:[0-9a-fA-F]){1,2}$/);
|
|
if (match && match.length === 1) {
|
|
hex_tuple.push(match[0]);
|
|
s_copy = s_copy.substring(0, match.index);
|
|
}
|
|
}
|
|
//var match = s.match(/^#(?:[0-9a-fA-F]{3}){1,2}$/);
|
|
if (hex_tuple.length === 3) {
|
|
// yeah positiv --> convert into right color spec
|
|
var color = [];
|
|
color[0] = parseInt(hex_tuple[0], 16) / 255.0;
|
|
color[1] = parseInt(hex_tuple[1], 16) / 255.0;
|
|
color[2] = parseInt(hex_tuple[2], 16) / 255.0;
|
|
|
|
return color.reverse();
|
|
} else {
|
|
throw new Sk.builtin.ValueError("invalid hex color string \"" + s + "\"");
|
|
}
|
|
};
|
|
|
|
/**
|
|
Expects and rgb tuple with values [0,1]
|
|
**/
|
|
Sk.jsplotlib.rgb2hex = function (rgb) {
|
|
if (!rgb) {
|
|
return null;
|
|
}
|
|
|
|
if (rgb.length && rgb.length >= 3) {
|
|
var i;
|
|
// some hacky code to rebuild string format :(
|
|
var hex_str = "#";
|
|
for (i = 0; i < 3; i++) {
|
|
var val = Math.round(rgb[i] * 255).toString(16);
|
|
hex_str += val.length == 2 ? val : "0" + val;
|
|
}
|
|
|
|
return hex_str;
|
|
}
|
|
}; |