Last active
October 5, 2017 16:31
-
-
Save MightyPork/43f90b134a7d870e9a4d1765eca5f0fb to your computer and use it in GitHub Desktop.
userscript pro GME.cz - graf ceny dle počtu kusů
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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