Skip to content

Instantly share code, notes, and snippets.

@MightyPork
Last active October 5, 2017 16:31
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MightyPork/43f90b134a7d870e9a4d1765eca5f0fb to your computer and use it in GitHub Desktop.
Save MightyPork/43f90b134a7d870e9a4d1765eca5f0fb to your computer and use it in GitHub Desktop.
userscript pro GME.cz - graf ceny dle počtu kusů
// ==UserScript==
// @name GME vykreslovač množstevní slevy
// @namespace http://tampermonkey.net/
// @version 3
// @description Množstevní sleva GME
// @author Ondřej Hruška, 2017
// @match https://www.gme.cz/*
// @grant MIT
// ==/UserScript==
// Web: https://gist.github.com/MightyPork/43f90b134a7d870e9a4d1765eca5f0fb
(function() {
'use strict';
// --- User configuration ---
const WANT_CONTINUATION_LINES = false;
// --------------------------
// Window size
const H = 450;
const W = 520;
// Internal spacing
const YOFF = 40; // Y offset
const WPAD = 15; // right padding
const WPADL = 40; // left padding
const CROSSW = 3; // cross size (marks break points)
const YPADT = 40; // top padding
// Colors
const C_PRICE = 'blue'; // Price tags text
const C_DISCOUNT = 'green'; // Discount info text
const C_DISCOUNT_BAD = 'red'; // Discount if negative
const C_AXIS = 'black'; // Horizontal axis color
const C_LEGEND = 'black'; // Legend numbers color (on axis)
const C_DOWNLINES = 'red'; //Lines going down from break points
const C_LINE0 = 'blue'; // Line gradient start color
const C_LINE1 = 'red'; // Line gradient end color
const C_LINE2 = WANT_CONTINUATION_LINES ? '#ccc' : 'white'; // Continuation line color
const C_MOUSE = "#0e0"; // Mouse cross color
const C_MOUSE_DK = "#090"; // Color of mouse preview info text
const C_ORDER = "black"; // Color of order info text
const CROSSW_ORDER = 5; // width of order marker cross
const C_ORDER_XYCROSS = C_MOUSE_DK;
// --- Status ---
// Mouse button held
let mb_held = 0;
let maxIndex = 0; // will be resolved during init
// --- utility functions ---
/** Draw a line with gradient color */
function gradientLine(ctx, x1, y1, x2, y2, c0, c1) {
var grd = ctx.createLinearGradient(x1, y1, x2, y2);
grd.addColorStop(0, c0);
grd.addColorStop(1, c1);
ctx.beginPath();
ctx.strokeStyle = grd;
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
function colorLine(ctx, x1, y1, x2, y2, c0) {
ctx.beginPath();
ctx.strokeStyle = c0;
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
/** Draw a cross */
function cross(ctx, x, y, color, armlen) {
ctx.beginPath();
ctx.strokeStyle = color;
ctx.moveTo(x-armlen, y);
ctx.lineTo(x+armlen, y);
ctx.moveTo(x, y-armlen);
ctx.lineTo(x, y+armlen);
ctx.stroke();
}
/** Draw a cross across the whole chart */
function xycross(ctx,x,y,color) {
colorLine(ctx,
x, 0,
x, y_bottom,
color);
colorLine(ctx,
x_start, y,
end_x, y,
color);
}
// --- Source data reading ---
let prices = $('.fee-price.row');
if (prices.length <= 0) return; // Abort if no prices are found
// Build info table
let tbl = [];
prices.each(function() {
let label = $(this).find('.label').text();
let pricet = $(this).find('.price').text();
let count = parseInt(label.match(/\d+/)[0]);
let price = parseFloat(pricet.match(/\d+([.,]\d+)?/)[0].replace(',', '.'));
tbl.unshift({n: count, p: price});
});
// add one more point to indicate the end direction
const xtran = 1.2;
tbl.push({n: tbl[tbl.length-1].n*xtran, p: tbl[tbl.length-1].p});
// --- DOM setup ---
let $sb = $('#row-product > .gallery > div:first-child');
// Building the canvas
let cvs = $(`<canvas width=${W+WPAD} height=${H}>`);
cvs.css({
border: "1px solid black",
marginLeft: "-10px",
marginBottom: "10px",
borderRadius: "5px",
});
let $wrap = $("<div style='clear:both; padding-top:20px;'>");
let $header = $("<div style='margin-bottom:5px;'>");
let $pickerLbl = $(`<label>Zoom (max počet ks): </label>`);
let $maxStopPicker = $(`<select>`);
for(let i=1;i<tbl.length;i++) {
// --- Here we find the initial max index and also pre-select the option ---
let sel = (tbl[i].n>=500 || i == tbl.length-1);
if (sel && maxIndex === 0) {
maxIndex = i;
} else {
sel = false;
}
$maxStopPicker.append(`<option value="${i}"${sel?' selected':''}>${tbl[i].n} ks</optio>`);
}
$header.append($pickerLbl);
$header.append($maxStopPicker);
$wrap.append($header);
$wrap.append(cvs);
$sb.append($wrap);
// input field with order count
let $countField = $('input[name="quantity"]');
let $priceField = $('#product-info .final-price .price');
$('#product-info .final-price .label > span').text("Cena celkem");
// --- Coordinate calc functions ---
// global variables updated in updateScale()
let yscale = 1;
let xscale = 1;
let coords = [];
let coords_end = [];
let end_y = [];
let end_n = tbl[maxIndex].n;
let end_x = 0;
let y_bottom = 0;
let x_start = 0;
/** Calculate X position from number of pieces */
const calcX = function(n) {
return Math.round(n*xscale)+0.5+WPADL;
};
/** Calculate number of pieces from X position */
const calcNfromX = function(x) {
return Math.round((x-WPADL)/xscale);
};
/** Calculate Y position from price */
const calcY = function(p) {
return Math.round(YOFF+(H-YOFF*2) - (p)*yscale)+0.5;
};
/** Calculate price from Y position */
const calcPfromY = function(y) {
return Math.round((y-(YOFF+(H-YOFF*2)))/yscale);
};
/** Calculate price from number of pieces */
const calcPfromN = function(n) {
let i = 0;
for(i=tbl.length-1;i>=0;i--) {
if(tbl[i].n<=n) {
break;
}
}
if (i<0) return -1;
const p0 = tbl[i].p;
const p = p0;
return p*n;
};
const updateScale = function (newMaxIndex) {
maxIndex = newMaxIndex;
let highestP = 0;
for(let i=1;i<=maxIndex;i++) {
let p = tbl[i].n * tbl[i-1].p;
if (p > highestP) highestP = p;
}
yscale = ((H-YOFF*2-YPADT)/(highestP));
xscale = (W-WPAD-WPADL)/tbl[maxIndex].n;
y_bottom = calcY(0);
x_start = calcX(0);
// --- Prepare canvas coordinates table ---
coords = [];
coords_end = [];
end_y = [];
end_n = tbl[maxIndex].n;
end_x = calcX(end_n);
for(let i=0;i<=maxIndex;i++) {
coords.push({
x: calcX(tbl[i].n),
y: calcY(tbl[i].p*tbl[i].n),
});
if (i<maxIndex) {
coords_end.push({
x: calcX(tbl[i+1].n),
y: calcY(tbl[i].p*tbl[i+1].n),
});
end_y.push(calcY(tbl[i].p*end_n));
}
}
};
updateScale(maxIndex);
// --- Drawing ---
let ctx = cvs[0].getContext('2d');
/**
* Redraw the canvas
* @param mn - mouse-over number of pieces, -1 if none
*/
function redraw(mn) {
let mp;
ctx.lineWidth = 1;
// --- Draw ---
// Erase all
ctx.clearRect(0, 0, W+WPAD, H);
// Info text in top left corner
const order_n = $countField.val(); // ordered pieces
const order_p = calcPfromN(order_n); // ordered pieces price
if (!mb_held) {
ctx.setLineDash([1,4]);
xycross(ctx, calcX(order_n), calcY(order_p), C_ORDER_XYCROSS);
ctx.setLineDash([]);
}
// Mouse cross
if (mn > 0 && mn <= end_n) {
mp = calcPfromN(mn);
const my = Math.round(calcY(mp))+0.5;
const mx = calcX(mn);
if (mp > 0) {
xycross(ctx, mx, my, C_MOUSE);
}
}
// Draw all segments
let t, dim;
for(let i=0;i<coords.length;i++) {
const c0 = coords[i];
const c1 = coords_end[i];
if (i < maxIndex) {
// Gradient line between points
gradientLine(ctx,
c0.x, c0.y,
c1.x, c1.y,
C_LINE0, C_LINE1);
// Continuation line (dashed)
ctx.setLineDash([1,2]);
gradientLine(ctx,
c1.x, c1.y,
end_x, end_y[i],
C_LINE2, C_LINE2);
ctx.setLineDash([]);
// End marker cross
cross(ctx, c1.x, c1.y, C_LINE1, CROSSW);
}
// Start marker cross
cross(ctx, c0.x, c0.y, C_LINE0, CROSSW);
// Line down (dased)
ctx.beginPath();
ctx.strokeStyle = C_DOWNLINES;
ctx.setLineDash([1,4]);
ctx.moveTo(coords[i].x, y_bottom);
ctx.lineTo(coords[i].x, coords[i].y);
ctx.stroke();
ctx.setLineDash([]);
// Total price at lower coord of segment
let prc = tbl[i].n*tbl[i].p;
// Price label
ctx.save();
ctx.translate(coords[i].x-7, coords[i].y-7);
ctx.rotate(Math.PI/4);
ctx.textAlign = "end";
ctx.font = '12px sans-serif';
t = `${Math.round(prc)} Kč`;
dim = ctx.measureText(t);
ctx.fillStyle = 'rgba(255,255,40,0.7)';//'#ffff66';
ctx.fillRect(-dim.width-1, -10, dim.width+2, 12);
ctx.fillStyle = C_PRICE;
ctx.fillText(t, 0, 0);
ctx.restore();
// Discount labels
if (i > 0) {
// Relative discount from previous segment
const discount = Math.round(prc-tbl[i].n*tbl[i-1].p);
if (discount !== 0) {
ctx.save();
ctx.translate(coords[i].x, coords[i].y+20);
ctx.textAlign = "center";
ctx.font = '11px sans-serif';
t = `${discount} Kč`;
dim = ctx.measureText(t);
ctx.fillStyle = 'white';
ctx.fillRect(-dim.width/2, -8, dim.width, 10);
ctx.fillStyle = discount < 0 ? C_DISCOUNT : C_DISCOUNT_BAD;
ctx.fillText(t, 0, 0);
ctx.restore();
}
// Relative discount from price at 1 piece
if (i > 1) {
const discount2 = Math.round(prc-tbl[i].n*tbl[0].p);
ctx.save();
ctx.translate(coords[i].x, coords[i].y+30);
ctx.textAlign = "center";
ctx.font = '11px sans-serif';
t = `(${discount2} Kč)`;
dim = ctx.measureText(t);
ctx.fillStyle = 'white';
ctx.fillRect(-dim.width/2, -8, dim.width, 10);
ctx.fillStyle = discount2 < 0 ? C_DISCOUNT : C_DISCOUNT_BAD;
ctx.fillText(t, 0, 0);
ctx.restore();
}
}
// Bottom label (number of pieces)
ctx.save();
ctx.font = '10px sans-serif';
ctx.fillStyle = C_LEGEND;
ctx.textAlign = "center";
ctx.translate(coords[i].x+0.5, y_bottom+15+0.5);
ctx.rotate(Math.PI/4);
ctx.fillText(tbl[i].n, 0, 0);
ctx.restore();
}
// X axis line
ctx.beginPath();
ctx.strokeStyle = C_AXIS;
ctx.moveTo(coords[0].x, y_bottom);
ctx.lineTo(coords[coords.length-1].x, y_bottom);
ctx.stroke();
// Top padding (erase too high parts of continuation lines)
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, W, 10);
ctx.save();
t = "Graf množstevní slevy";
ctx.font = '19px sans-serif';
ctx.textAlign = "left";
// background white fill behind texts
ctx.fillStyle = 'rgba(255,255,255,0.7)';
ctx.fillRect(10, 10, 250, 60);
ctx.fillStyle = C_LEGEND;
ctx.translate(10, 25);
ctx.fillText(t, 0, 0);
ctx.translate(0, 20);
ctx.font = '14px sans-serif';
const order_pr = Math.round(order_p);
const order_mj = Math.round(order_p*100/order_n)/100;
ctx.fillText(`Objednávka: ${order_n} ks, ${order_pr} Kč (${order_mj} Kč/ks)`, 0, 0);
$priceField.html(`~${order_pr} Kč <small>(${order_mj} Kč/ks)</small>`);
if (mn > 0 && mn <= end_n) {
ctx.translate(0, 18);
ctx.fillStyle = C_MOUSE_DK;
ctx.fillText(`Náhled: ${mn} ks, ${Math.round(mp)} Kč (${Math.round(mp*100/mn)/100} Kč/ks)`, 0, 0);
}
ctx.restore();
// Order count marker on a segment
ctx.save();
cross(ctx, calcX(order_n), calcY(order_p), C_ORDER, CROSSW_ORDER);
ctx.restore();
}
// initial redraw
redraw(-1);
// --- Mouse interactivity ---
$maxStopPicker.on('change', function(e) {
updateScale($maxStopPicker.val());
redraw();
});
/**
* Put new value in the order input field
* @param e - mouse event that triggered this
*/
function setOrderCount(e) {
if(e.offsetY >0&&e.offsetY<H) {
const n = calcNfromX(e.offsetX);
if (n > 0) {
$countField.val(n);
}
}
}
/** Calculate N from mouse X coord of event e */
function calcEventXN(e) {
return (e.offsetY <0||e.offsetY>H) ?-1 : calcNfromX(e.offsetX);
}
// Update order count marker
$countField.on("input valuechange keyup keypress", function() {
redraw();
});
// Update mouse cross on move
cvs.on("mousemove mouseout mouseleave", function(e) {
redraw(calcEventXN(e));
});
// Update order count on move with held button
cvs.on("mousemove", function(e) {
if(mb_held) {
setOrderCount(e);
redraw(calcEventXN(e));
}
});
// Click on canvas
cvs.on("click", function(e) {
setOrderCount(e);
redraw(calcEventXN(e));
});
// Mouse pressed - button state tracking
cvs.on("mousedown", function(e) {
mb_held = 1;
});
// Mouse released - button state tracking
cvs.on("mouseup", function(e) {
mb_held = 0;
});
// Mouse left canvas - button state tracking
cvs.on("mouseleave", function(e) {
mb_held = 0;
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment