Skip to content

Instantly share code, notes, and snippets.

@habdelra
Created June 13, 2025 19:04
Show Gist options
  • Select an option

  • Save habdelra/6d8c99d81f05e2ec9147c182a8f4a23c to your computer and use it in GitHub Desktop.

Select an option

Save habdelra/6d8c99d81f05e2ec9147c182a8f4a23c to your computer and use it in GitHub Desktop.
import { eq, not } from '@cardstack/boxel-ui/helpers';
// ═══ [EDIT TRACKING: ON] Mark all changes with ⁽ⁿ⁾ ═══
import {
CardDef,
field,
contains,
Component,
} from 'https://cardstack.com/base/card-api'; // ⁽¹⁾ Core imports
import StringField from 'https://cardstack.com/base/string';
import NumberField from 'https://cardstack.com/base/number';
import CalculatorIcon from '@cardstack/boxel-icons/calculator'; // ⁽²⁾ Icon import
import { Button } from '@cardstack/boxel-ui/components';
import { fn, concat } from '@ember/helper';
import { on } from '@ember/modifier';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
class Isolated extends Component<typeof GraphingCalculator> {
// ⁽⁷⁾ Main calculator interface
@tracked functionInput = this.args.model?.currentFunction ?? 'x^2';
@tracked xMinInput = String(this.args.model?.xMin ?? -10);
@tracked xMaxInput = String(this.args.model?.xMax ?? 10);
@tracked yMinInput = String(this.args.model?.yMin ?? -10);
@tracked yMaxInput = String(this.args.model?.yMax ?? 10);
@tracked gridSpacingInput = String(this.args.model?.gridSpacing ?? 1);
@tracked calculationResult = '';
@tracked expressionInput = '';
@tracked isGraphing = false;
@tracked errorMessage = '';
// ⁽⁸⁾ Function parsing and evaluation
evaluateFunction(x: number): number | null {
try {
let expr = this.functionInput
.replace(/\^/g, '**')
.replace(/sin/g, 'Math.sin')
.replace(/cos/g, 'Math.cos')
.replace(/tan/g, 'Math.tan')
.replace(/log/g, 'Math.log10')
.replace(/ln/g, 'Math.log')
.replace(/sqrt/g, 'Math.sqrt')
.replace(/abs/g, 'Math.abs')
.replace(/pi/g, 'Math.PI')
.replace(/e(?![a-zA-Z])/g, 'Math.E')
.replace(/\bx\b/g, x.toString());
const result = eval(expr);
return isFinite(result) ? result : null;
} catch (e) {
return null;
}
}
// ⁽⁹⁾ Graph plotting calculations
get graphPoints(): Array<{ x: number; y: number }> {
const points = [];
const steps = 400;
const xMin = parseFloat(this.xMinInput);
const xMax = parseFloat(this.xMaxInput);
const yMin = parseFloat(this.yMinInput);
const yMax = parseFloat(this.yMaxInput);
const stepSize = (xMax - xMin) / steps;
for (let i = 0; i <= steps; i++) {
const x = xMin + i * stepSize;
const y = this.evaluateFunction(x);
if (y !== null && y >= yMin && y <= yMax) {
points.push({ x, y });
}
}
return points;
}
// ⁽¹⁰⁾ SVG coordinate transformations
scaleX(x: number): number {
const width = 600;
const xMin = parseFloat(this.xMinInput);
const xMax = parseFloat(this.xMaxInput);
return ((x - xMin) / (xMax - xMin)) * width;
}
scaleY(y: number): number {
const height = 400;
const yMin = parseFloat(this.yMinInput);
const yMax = parseFloat(this.yMaxInput);
return height - ((y - yMin) / (yMax - yMin)) * height;
}
// ⁽¹¹⁾ Grid line calculations
get gridLines(): Array<{
type: 'vertical' | 'horizontal';
position: number;
value: number;
}> {
const lines = [];
const spacing = this.gridSpacingInput;
// Vertical grid lines
for (
let x = Math.ceil(this.xMinInput / spacing) * spacing;
x <= this.xMaxInput;
x += spacing
) {
if (x >= this.xMinInput && x <= this.xMaxInput) {
lines.push({ type: 'vertical', position: this.scaleX(x), value: x });
}
}
// Horizontal grid lines
for (
let y = Math.ceil(this.yMinInput / spacing) * spacing;
y <= this.yMaxInput;
y += spacing
) {
if (y >= this.yMinInput && y <= this.yMaxInput) {
lines.push({ type: 'horizontal', position: this.scaleY(y), value: y });
}
}
return lines;
}
// ⁽¹²⁾ Path string for SVG
get graphPath(): string {
const points = this.graphPoints;
if (points.length === 0) return '';
let path = `M ${this.scaleX(points[0].x)} ${this.scaleY(points[0].y)}`;
for (let i = 1; i < points.length; i++) {
const x = this.scaleX(points[i].x);
const y = this.scaleY(points[i].y);
path += ` L ${x} ${y}`;
}
return path;
}
// ⁽¹³⁾ Action handlers
updateFunction = (event: Event) => {
const target = event.target as HTMLInputElement;
this.functionInput = target.value;
this.errorMessage = '';
this.updateModel();
};
updateXMin = (event: Event) => {
const target = event.target as HTMLInputElement;
this.xMinInput = String(parseFloat(target.value) || -10);
};
updateXMax = (event: Event) => {
const target = event.target as HTMLInputElement;
this.xMaxInput = String(parseFloat(target.value) || 10);
};
updateYMin = (event: Event) => {
const target = event.target as HTMLInputElement;
this.yMinInput = String(parseFloat(target.value) || -10);
};
updateYMax = (event: Event) => {
const target = event.target as HTMLInputElement;
this.yMaxInput = String(parseFloat(target.value) || 10);
};
updateGridSpacing = (event: Event) => {
const target = event.target as HTMLInputElement;
this.gridSpacingInput = String(parseFloat(target.value) || 1);
};
updateExpression = (event: Event) => {
const target = event.target as HTMLInputElement;
this.expressionInput = target.value;
};
updateViewport = () => {
this.updateModel();
};
calculateExpression = () => {
if (!this.expressionInput.trim()) return;
try {
const expr = this.expressionInput
.replace(/\^/g, '**')
.replace(/sin/g, 'Math.sin')
.replace(/cos/g, 'Math.cos')
.replace(/tan/g, 'Math.tan')
.replace(/log/g, 'Math.log10')
.replace(/ln/g, 'Math.log')
.replace(/sqrt/g, 'Math.sqrt')
.replace(/pi/g, 'Math.PI')
.replace(/e(?![a-zA-Z])/g, 'Math.E');
const result = eval(expr);
this.calculationResult = isFinite(result)
? result.toString()
: 'Invalid result';
} catch (e) {
this.calculationResult = 'Error: Invalid expression';
}
};
zoomIn = () => {
const centerX = (this.xMaxInput + this.xMinInput) / 2;
const centerY = (this.yMaxInput + this.yMinInput) / 2;
const rangeX = (this.xMaxInput - this.xMinInput) * 0.8;
const rangeY = (this.yMaxInput - this.yMinInput) * 0.8;
this.xMinInput = centerX - rangeX / 2;
this.xMaxInput = centerX + rangeX / 2;
this.yMinInput = centerY - rangeY / 2;
this.yMaxInput = centerY + rangeY / 2;
this.updateViewport();
};
zoomOut = () => {
const centerX = (this.xMaxInput + this.xMinInput) / 2;
const centerY = (this.yMaxInput + this.yMinInput) / 2;
const rangeX = (this.xMaxInput - this.xMinInput) * 1.25;
const rangeY = (this.yMaxInput - this.yMinInput) * 1.25;
this.xMinInput = centerX - rangeX / 2;
this.xMaxInput = centerX + rangeX / 2;
this.yMinInput = centerY - rangeY / 2;
this.yMaxInput = centerY + rangeY / 2;
this.updateViewport();
};
resetView = () => {
this.xMinInput = -10;
this.xMaxInput = 10;
this.yMinInput = -10;
this.yMaxInput = 10;
this.gridSpacingInput = 1;
this.updateViewport();
};
insertFunction = (func: string) => {
this.functionInput = func;
this.updateModel();
};
updateModel() {
if (this.args.model) {
this.args.model.currentFunction = this.functionInput;
this.args.model.xMin = this.xMinInput;
this.args.model.xMax = this.xMaxInput;
this.args.model.yMin = this.yMinInput;
this.args.model.yMax = this.yMaxInput;
this.args.model.gridSpacing = this.gridSpacingInput;
}
}
<template>
<!-- ⁽¹⁴⁾ Calculator interface layout -->
<div class='calculator-stage'>
<div class='calculator-container'>
<header class='calculator-header'>
<h1>
<svg
class='calc-icon'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
stroke-width='2'
>
<rect x='4' y='4' width='16' height='16' rx='2' />
<rect x='9' y='9' width='1' height='1' />
<rect x='14' y='9' width='1' height='1' />
<rect x='9' y='14' width='1' height='1' />
<rect x='14' y='14' width='1' height='1' />
</svg>
Advanced Graphing Calculator
</h1>
</header>
<div class='calculator-layout'>
<!-- ⁽¹⁵⁾ Function input and controls -->
<div class='controls-panel'>
<section class='function-section'>
<label class='input-label'>Function: y =</label>
<input
type='text'
class='function-input'
value={{this.functionInput}}
placeholder='Enter function (e.g., x^2, sin(x), 2*x+1)'
{{on 'input' this.updateFunction}}
/>
<div class='function-buttons'>
<Button
class='func-btn'
{{on 'click' (fn this.insertFunction 'x^2')}}
>x²</Button>
<Button
class='func-btn'
{{on 'click' (fn this.insertFunction 'x^3')}}
>x³</Button>
<Button
class='func-btn'
{{on 'click' (fn this.insertFunction 'sin(x)')}}
>sin(x)</Button>
<Button
class='func-btn'
{{on 'click' (fn this.insertFunction 'cos(x)')}}
>cos(x)</Button>
<Button
class='func-btn'
{{on 'click' (fn this.insertFunction 'tan(x)')}}
>tan(x)</Button>
<Button
class='func-btn'
{{on 'click' (fn this.insertFunction 'sqrt(x)')}}
>√x</Button>
<Button
class='func-btn'
{{on 'click' (fn this.insertFunction 'abs(x)')}}
>|x|</Button>
<Button
class='func-btn'
{{on 'click' (fn this.insertFunction 'log(x)')}}
>log(x)</Button>
</div>
</section>
<!-- ⁽¹⁶⁾ Viewport controls -->
<section class='viewport-section'>
<h3>Viewport</h3>
<div class='viewport-controls'>
<div class='range-control'>
<label>X: </label>
<input
type='number'
class='range-input'
value={{this.xMinInput}}
{{on 'input' this.updateXMin}}
{{on 'blur' this.updateViewport}}
/>
<span>to</span>
<input
type='number'
class='range-input'
value={{this.xMaxInput}}
{{on 'input' this.updateXMax}}
{{on 'blur' this.updateViewport}}
/>
</div>
<div class='range-control'>
<label>Y: </label>
<input
type='number'
class='range-input'
value={{this.yMinInput}}
{{on 'input' this.updateYMin}}
{{on 'blur' this.updateViewport}}
/>
<span>to</span>
<input
type='number'
class='range-input'
value={{this.yMaxInput}}
{{on 'input' this.updateYMax}}
{{on 'blur' this.updateViewport}}
/>
</div>
<div class='range-control'>
<label>Grid: </label>
<input
type='number'
class='range-input'
value={{this.gridSpacingInput}}
{{on 'input' this.updateGridSpacing}}
{{on 'blur' this.updateViewport}}
/>
</div>
</div>
<div class='zoom-controls'>
<Button class='zoom-btn' {{on 'click' this.zoomIn}}>Zoom In</Button>
<Button class='zoom-btn' {{on 'click' this.zoomOut}}>Zoom Out</Button>
<Button
class='zoom-btn reset'
{{on 'click' this.resetView}}
>Reset</Button>
</div>
</section>
<!-- ⁽¹⁷⁾ Calculator -->
<section class='calc-section'>
<h3>Calculator</h3>
<div class='calc-input-group'>
<input
type='text'
class='calc-input'
value={{this.expressionInput}}
placeholder='Enter expression to calculate'
{{on 'input' this.updateExpression}}
/>
<Button
class='calc-btn'
{{on 'click' this.calculateExpression}}
>Calculate</Button>
</div>
{{#if this.calculationResult}}
<div class='calc-result'>= {{this.calculationResult}}</div>
{{/if}}
</section>
</div>
<!-- ⁽¹⁸⁾ Graph display -->
<div class='graph-panel'>
<div class='graph-container'>
<svg class='graph-svg' viewBox='0 0 600 400'>
<!-- Grid lines -->
<defs>
<pattern
id='grid'
width='20'
height='20'
patternUnits='userSpaceOnUse'
>
<path
d='M 20 0 L 0 0 0 20'
fill='none'
stroke='#e5e7eb'
stroke-width='1'
/>
</pattern>
</defs>
<!-- Grid background -->
<rect width='600' height='400' fill='url(#grid)' />
<!-- Grid lines with labels -->
{{#each this.gridLines as |line|}}
{{#if (eq line.type 'vertical')}}
<line
x1={{line.position}}
y1='0'
x2={{line.position}}
y2='400'
stroke='#d1d5db'
stroke-width='1'
/>
{{#if (not (eq line.value 0))}}
<text
x={{line.position}}
y='395'
text-anchor='middle'
font-size='10'
fill='#6b7280'
>{{line.value}}</text>
{{/if}}
{{else}}
<line
x1='0'
y1={{line.position}}
x2='600'
y2={{line.position}}
stroke='#d1d5db'
stroke-width='1'
/>
{{#if (not (eq line.value 0))}}
<text
x='5'
y={{line.position}}
text-anchor='start'
dominant-baseline='middle'
font-size='10'
fill='#6b7280'
>{{line.value}}</text>
{{/if}}
{{/if}}
{{/each}}
<!-- Axes -->
<line
x1={{this.scaleX 0}}
y1='0'
x2={{this.scaleX 0}}
y2='400'
stroke='#374151'
stroke-width='2'
/>
<line
x1='0'
y1={{this.scaleY 0}}
x2='600'
y2={{this.scaleY 0}}
stroke='#374151'
stroke-width='2'
/>
<!-- Function graph -->
{{#if this.graphPath}}
<path
d={{this.graphPath}}
stroke='#3b82f6'
stroke-width='2'
fill='none'
/>
{{/if}}
</svg>
</div>
</div>
</div>
</div>
</div>
<style scoped>
/* ⁽¹⁹⁾ Professional calculator styling */
.calculator-stage {
width: 100%;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
font-family:
'Inter',
-apple-system,
sans-serif;
}
.calculator-container {
background: white;
border-radius: 16px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
width: 100%;
max-width: 1200px;
height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.calculator-header {
background: linear-gradient(135deg, #1e40af, #3730a3);
color: white;
padding: 1rem 2rem;
border-radius: 16px 16px 0 0;
}
.calculator-header h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.75rem;
}
.calc-icon {
width: 2rem;
height: 2rem;
}
.calculator-layout {
display: grid;
grid-template-columns: 350px 1fr;
height: 100%;
}
.controls-panel {
background: #f8fafc;
border-right: 1px solid #e2e8f0;
padding: 1.5rem;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.function-section h3,
.viewport-section h3,
.calc-section h3 {
margin: 0 0 1rem 0;
font-size: 1rem;
font-weight: 600;
color: #1e293b;
}
.input-label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: #374151;
margin-bottom: 0.5rem;
}
.function-input {
width: 100%;
padding: 0.75rem;
border: 2px solid #d1d5db;
border-radius: 8px;
font-size: 0.875rem;
font-family: 'Monaco', 'Menlo', monospace;
transition: border-color 0.2s ease;
}
.function-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.function-buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
margin-top: 1rem;
}
/* ⁽²⁰⁾ Button styling */
.func-btn {
background: #e2e8f0;
border: 1px solid #cbd5e1;
border-radius: 6px;
padding: 0.5rem;
font-size: 0.75rem;
font-weight: 500;
color: #475569;
cursor: pointer;
transition: all 0.2s ease;
}
.func-btn:hover {
background: #cbd5e1;
border-color: #94a3b8;
transform: translateY(-1px);
}
.viewport-controls {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.range-control {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
}
.range-control label {
min-width: 1.5rem;
font-weight: 500;
color: #374151;
}
.range-input {
width: 4rem;
padding: 0.25rem 0.5rem;
border: 1px solid #d1d5db;
border-radius: 4px;
font-size: 0.75rem;
text-align: center;
}
.zoom-controls {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}
.zoom-btn {
background: #3b82f6;
color: white;
border: none;
border-radius: 6px;
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
flex: 1;
}
.zoom-btn:hover {
background: #2563eb;
transform: translateY(-1px);
}
.zoom-btn.reset {
background: #ef4444;
}
.zoom-btn.reset:hover {
background: #dc2626;
}
.calc-input-group {
display: flex;
gap: 0.5rem;
}
.calc-input {
flex: 1;
padding: 0.5rem;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.875rem;
font-family: 'Monaco', 'Menlo', monospace;
}
.calc-btn {
background: #059669;
color: white;
border: none;
border-radius: 6px;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.calc-btn:hover {
background: #047857;
}
.calc-result {
margin-top: 0.75rem;
padding: 0.75rem;
background: #ecfdf5;
border: 1px solid #a7f3d0;
border-radius: 6px;
font-family: 'Monaco', 'Menlo', monospace;
font-weight: 600;
color: #065f46;
}
.graph-panel {
background: white;
padding: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
}
.graph-container {
width: 100%;
height: 100%;
max-width: 600px;
max-height: 400px;
border: 2px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
background: white;
}
.graph-svg {
width: 100%;
height: 100%;
display: block;
}
/* ⁽²¹⁾ Responsive design */
@media (max-width: 1000px) {
.calculator-layout {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
.controls-panel {
border-right: none;
border-bottom: 1px solid #e2e8f0;
overflow-y: visible;
}
}
@media (max-width: 600px) {
.function-buttons {
grid-template-columns: repeat(2, 1fr);
}
.zoom-controls {
flex-direction: column;
}
}
</style>
</template>
}
class Embedded extends Component<typeof GraphingCalculator> {
// ⁽²²⁾ Compact view
<template>
<div class='calculator-card'>
<div class='card-header'>
<svg
class='card-icon'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
stroke-width='2'
>
<rect x='4' y='4' width='16' height='16' rx='2' />
<rect x='9' y='9' width='1' height='1' />
<rect x='14' y='9' width='1' height='1' />
</svg>
<h3>{{if
@model.currentFunction
(concat 'f(x) = ' @model.currentFunction)
'Graphing Calculator'
}}</h3>
</div>
<div class='mini-graph'>
<svg viewBox='0 0 200 120'>
<rect width='200' height='120' fill='#f8fafc' stroke='#e2e8f0' />
<line x1='100' y1='0' x2='100' y2='120' stroke='#d1d5db' />
<line x1='0' y1='60' x2='200' y2='60' stroke='#d1d5db' />
<path
d='M 0 120 Q 50 60 100 60 T 200 0'
stroke='#3b82f6'
stroke-width='2'
fill='none'
/>
</svg>
</div>
<div class='card-info'>
<span>Range:
{{if
@model.xMin
(concat @model.xMin ' to ' @model.xMax)
'Default view'
}}</span>
</div>
</div>
<style scoped>
/* ⁽²³⁾ Card styles */
.calculator-card {
background: white;
border-radius: 8px;
padding: 1rem;
font-size: 0.8125rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.card-icon {
width: 1.25rem;
height: 1.25rem;
color: #3b82f6;
}
.card-header h3 {
margin: 0;
font-size: 0.875rem;
font-weight: 600;
color: #1e293b;
}
.mini-graph {
margin-bottom: 0.75rem;
}
.mini-graph svg {
width: 100%;
height: 80px;
border-radius: 4px;
}
.card-info {
font-size: 0.75rem;
color: #6b7280;
text-align: center;
}
</style>
</template>
}
export class GraphingCalculator extends CardDef {
// ⁽³⁾ Calculator definition
static displayName = 'Graphing Calculator';
static icon = CalculatorIcon;
static prefersWideFormat = true;
@field currentFunction = contains(StringField); // ⁽⁴⁾ Function expression
@field xMin = contains(NumberField); // ⁽⁵⁾ Viewport bounds
@field xMax = contains(NumberField);
@field yMin = contains(NumberField);
@field yMax = contains(NumberField);
@field gridSpacing = contains(NumberField);
// ⁽⁶⁾ Computed title from current function
@field title = contains(StringField, {
computeVia: function (this: GraphingCalculator) {
try {
const func = this.currentFunction ?? 'y = x';
return `Graphing Calculator - ${func}`;
} catch (e) {
console.error('GraphingCalculator: Error computing title', e);
return 'Graphing Calculator';
}
},
});
static isolated = Isolated;
static embedded = Embedded;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment