From fb12106fdc323e662d3ed60fd7634c1546957413 Mon Sep 17 00:00:00 2001 From: Joana Bergsiek Date: Tue, 17 Oct 2023 14:43:32 +0200 Subject: [PATCH] Adds a notation and scale --- .../Sandblocks-Watch/SBAxisNotation.class.st | 98 +++++++++++++++++++ packages/Sandblocks-Watch/SBScale.class.st | 98 +++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 packages/Sandblocks-Watch/SBAxisNotation.class.st create mode 100644 packages/Sandblocks-Watch/SBScale.class.st diff --git a/packages/Sandblocks-Watch/SBAxisNotation.class.st b/packages/Sandblocks-Watch/SBAxisNotation.class.st new file mode 100644 index 00000000..b48039a2 --- /dev/null +++ b/packages/Sandblocks-Watch/SBAxisNotation.class.st @@ -0,0 +1,98 @@ +" +A numerical legend for a scale +" +Class { + #name : #SBAxisNotation, + #superclass : #Morph, + #instVars : [ + 'scale', + 'numberTicks' + ], + #category : #'Sandblocks-Watch' +} + +{ #category : #'initialize-release' } +SBAxisNotation class >> newFromScale: aSBScale ticking: aNumber [ + + ^ self new + scale: aSBScale + numberTicks: aNumber +] + +{ #category : #initialization } +SBAxisNotation >> initialize [ + + super initialize. + + numberTicks := 3. + scale := SBScale newLinearScaleWithDomain: (0 to: 100) forRange: (0 to: 100). + + self color: Color transparent; + layoutPolicy: ProportionalLayout new; + hResizing: #shrinkWrap; + vResizing: #spaceFill +] + +{ #category : #accessing } +SBAxisNotation >> numberTicks [ + + ^ numberTicks +] + +{ #category : #accessing } +SBAxisNotation >> numberTicks: aNumber [ + + numberTicks := aNumber. + + self visualize +] + +{ #category : #visualization } +SBAxisNotation >> relativeTickHeights [ + + | section adjustedTicks | + (self numberTicks < 2) ifTrue: [^#()]. + + "Starting count from 0 here instead of 1" + adjustedTicks := self numberTicks - 1. + section := 1 / adjustedTicks. + ^ (0 to: adjustedTicks) collect: [:i | (section * i)] +] + +{ #category : #accessing } +SBAxisNotation >> scale [ + + ^ scale +] + +{ #category : #accessing } +SBAxisNotation >> scale: aSBScale [ + + scale := aSBScale. + + self visualize +] + +{ #category : #accessing } +SBAxisNotation >> scale: aSBScale numberTicks: aNumber [ + + scale := aSBScale. + numberTicks := aNumber. + + self visualize +] + +{ #category : #visualization } +SBAxisNotation >> visualize [ + + self submorphs copy do: #abandon. + self relativeTickHeights withIndexDo: [:aFraction :i | | annotation | + annotation := SBOwnTextMorph new + contents: ((self scale scaledValueOfRelative: aFraction) asString); + layoutFrame: (LayoutFrame new topFraction: aFraction). + "Fraction 1 will result in the text being just underneath ourself, so substract height as offset" + (aFraction = 1) ifTrue: [annotation layoutFrame topOffset: (-1*(annotation minExtent y))]. + self addMorph: annotation.] + + +] diff --git a/packages/Sandblocks-Watch/SBScale.class.st b/packages/Sandblocks-Watch/SBScale.class.st new file mode 100644 index 00000000..f861ac2d --- /dev/null +++ b/packages/Sandblocks-Watch/SBScale.class.st @@ -0,0 +1,98 @@ +" +A scale inspired by their usage in d3. Define a domain from which a set of numbers gets projected to your defined range. Both are Interval objects. As operations like min or array accesses don't work on reversed ordered Intervals (eg meaning they're sorted descendingly), only ascending intervals work. + +A scale behavior is a block accepting a domain and a value to scale. The output is a percentage [0;1] representing its relative position in your domain. It can scale the value linear, logarithmic, exponentially etc. For an example, view class > linearScale. +" +Class { + #name : #SBScale, + #superclass : #Object, + #instVars : [ + 'range', + 'domain', + 'scaleBehavior' + ], + #category : #'Sandblocks-Watch' +} + +{ #category : #scaleBehaviors } +SBScale class >> linearScaleBehavior [ + + ^ [:domain :value | ((value - domain min) / {domain extent. 1} max) abs] +] + +{ #category : #'initialize-release' } +SBScale class >> newLinearScaleWithDomain: aDomainInterval forRange: aRangeInterval [ + + ^ self new + domain: ( + {aDomainInterval start. aDomainInterval stop} min + to: {aDomainInterval start. aDomainInterval stop} max); + range: ( + {aRangeInterval start. aRangeInterval stop} min + to: {aRangeInterval start. aRangeInterval stop} max); + scaleBehavior: self linearScaleBehavior; + yourself +] + +{ #category : #'initialize-release' } +SBScale class >> newWithDomain: aDomainInterval forRange: aRangeInterval scalingLike: aScaleBehavior [ + + ^ self new + domain: aDomainInterval; + range: aRangeInterval; + scaleBehavior: aScaleBehavior; + yourself. +] + +{ #category : #accessing } +SBScale >> domain [ + + ^ domain +] + +{ #category : #accessing } +SBScale >> domain: anInterval [ + + domain := anInterval +] + +{ #category : #accessing } +SBScale >> range [ + + ^ range +] + +{ #category : #accessing } +SBScale >> range: anInterval [ + + range := anInterval +] + +{ #category : #accessing } +SBScale >> scaleBehavior [ + + ^ scaleBehavior +] + +{ #category : #accessing } +SBScale >> scaleBehavior: aBlock [ + + scaleBehavior := aBlock +] + +{ #category : #calculating } +SBScale >> scaledValueOf: aNumberInDomain [ + + "In case the given number is the start of the domain, meaning always at 0% of total range, + we might get a ZeroDivide during calculations, so catch the case early by return." + (aNumberInDomain = self domain first) ifTrue: [^ self range first]. + + ^ self range at: (self scaleBehavior value: self domain value: aNumberInDomain) * self range extent +1 +] + +{ #category : #calculating } +SBScale >> scaledValueOfRelative: aNumberFrom0To1 [ + + "LERP the given number in domain before proceeding normally" + ^ self scaledValueOf: ((1 - aNumberFrom0To1) * self domain min + aNumberFrom0To1 * self domain max) +]